Java多线程创建和常用方法

本文主要介绍线程的定义,创建,使用,停止,状态图和常用方法。
主要用于概念扫盲和梳理。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。

先说并发

1. 为什么要用到并发

一直以来,硬件的发展极其迅速,也有一个很著名的"摩尔定律",可能会奇怪明明讨论的是并发编程为什么会扯到了硬件的发展,这其中的关系应该是多核CPU的发展为并发编程提供的硬件基础。摩尔定律并不是一种自然法则或者是物理定律,它只是基于认为观测数据后,对未来的一种预测。
按照所预测的速度,我们的计算能力会按照指数级别的速度增长,不久以后会拥有超强的计算能力,正是在畅想未来的时候,2004年,Intel宣布4GHz芯片的计划推迟到2005年,然后在2004年秋季,Intel宣布彻底取消4GHz的计划,也就是说摩尔定律的有效性超过了半个世纪戛然而止

但是,聪明的硬件工程师并没有停止研发的脚步,他们为了进一步提升计算速度,而不是再追求单独的计算单元,而是将多个计算单元整合到了一起,也就是形成了多核CPU。短短十几年的时间,家用型CPU,比如Intel i7就可以达到4核心甚至8核心。而专业服务器则通常可以达到几个独立的CPU,每一个CPU甚至拥有多达8个以上的内核。因此,摩尔定律似乎在CPU核心扩展上继续得到体验。因此,多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。
顶级计算机科学家Donald Ervin Knuth如此评价这种情况:在我看来,这种现象(并发)或多或少是由于硬件设计者无计可施了导致的,他们将摩尔定律的责任推给了软件开发者。
另外,在特殊的业务场景下先天的就适合于并发编程。比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素。即时将所有的像素遍历一边都需要很长的时间,面对如此复杂的计算量就需要充分利用多核的计算的能力。又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。

2. 并发编程缺点

  • 频繁的上下文切换
    时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。

通常减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程。

  1. 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
  2. CAS算法,利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换
  3. 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态
  4. 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
  • 线程安全

3. 一些概念

3.1 同步与异步

同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。
比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。

3.2 并发与并行

并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。
真正的并行也只能出现在拥有多个CPU的系统中。

3.3 阻塞和非阻塞

阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行

3.4 临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

进程与线程

进程--资源分配的最小单位

  • 每个进程都有独立的代码和数据空间(进程上下文)
  • 一个进程包含多个线程
  • 进程间的切换会有较大的开销

线程--cpu调度的最小单位

  • 同一类线程共享代码和数据空间,
  • 每个线程有独立的运行栈和程序计数器(PC)
  • 线程切换开销小

其他

  • 多进程是指操作系统能同时运行多个任务(程序)。
  • 多线程是指在同一程序中有多个顺序流在执行。
  • 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

线程创建、使用、停止

创建线程

  • 在Java中要想实现多线程,有三种手段,一种是继承Thread类,另外一种是实现Runnable接口,三是实现Callable 接口。
    实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

1. 继承Thread 类,覆盖run方法(推荐使用Runable)

  • run()方法是多线程程序的一个约定,所有的多线程代码都在run方法里面。
        class PrimeThread extends Thread {
         long minPrime;
         PrimeThread(long minPrime) {
             this.minPrime = minPrime;
         }

         public void run() {
             // compute primes larger than minPrime
              . . .
         }
     }
  • 我们复写的是run()方法,但是执行的却是start()方法。
  • start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
     PrimeThread p = new PrimeThread(143);
     p.start();

2. 实现Runnable接口,覆盖run方法

     class PrimeRun implements Runnable {
         long minPrime;
         PrimeRun(long minPrime) {
             this.minPrime = minPrime;
         }

         public void run() {
             // compute primes larger than minPrime
              . . .
         }
     }

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

     PrimeRun p = new PrimeRun(143);
     new Thread(p).start();

    new Thread(new PrimeRun(143)).start();

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础

3. 实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过FutureTask 进行封装

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        // ...
    }
    public static void main(String[]  args) {
        MyCallable mc = new MyCallable();
        FutureTask<Integer> ft = new FutureTask<>(mc);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
}

Thread和Runnable的区别

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去处理同一个资源
  2. 可以避免java中的单继承的限制
  3. 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
  4. 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

使用线程

方法

类别 方法签名 简介
构造方法 thread() -
- thread(String name) -
- thread(Runnable target) -
- thread(Runnable target,String name) -
常用方法 void start() 启动线程
- static void sleep(long millis) 休眠
- static void sleep(long millis,int nanos) 休眠
- void join() 使其他线程等待当前线程终止
- void join(long millis) 使其他线程等待当前线程终止
- void join(long millis,int nanos) 使其他线程等待当前线程终止
- static void yield( ) 当前运行线程释放处理器资源
获取线程引用 static Thread currendThread() 返回当前运行的线程引用

注意事项

  • main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。
  • 在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。

停止线程

错误方法

  • stop()方法。
    它会使线程戛然而止,我们不知道它完成了什么工作,没有完成什么工作,因此是错误的。
  • interrupt()方法。
    不要使用interrupt()方法,因为在线程run()中调用sleep(),join(),yeild()方法时,中断状态会被清除isinterrupe=false

如何正确的停止线程

使用退出标志。

volatile boolean keepRunning = true;

代码示例

public class ActorThread extends Thread {//继承Thread创建线程
    // 覆盖run------------------------------------------------------
    public void run() {
        System.out.println(getName() + "是一个演员!");
        int count = 0;
        boolean keepRunning = true;
        while (keepRunning) {
            System.out.println(getName() + "登台演出:" + (++count));
            //标注法停止线程-----------------------------------------------
            if (count == 15) {
                keepRunning = false;
            }
            //测试sleep()-----------------------
            if (count % 5 == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(getName() + "的演出结束了!");
    }

    public static void main(String[] args) {
        Thread actor = new ActorThread();
        actor.setName("Mr. Thread");
        actor.start();
        Thread actressThread = new Thread(new Actress(), "Ms. Runnable");
        actressThread.start();
    }

}

class Actress implements Runnable {//实现Runnable接口创建线程
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "是一个演员!");
        int count = 0;
        boolean keepRunning = true;
        while (keepRunning) {
            System.out.println(Thread.currentThread().getName() + "登台演出:" + (++count));
            if (count == 15) {
                keepRunning = false;
            }
            if (count % 5 == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(Thread.currentThread().getName() + "的演出结束了!");
    }
}

输出

测试sleep方法与停止线程

线程状态转换

线程状态转换
  1. 新建状态(New):新创建了一个线程对象。
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(sleep是不会释放持有的锁)
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

常用函数说明

sleep(long millis):线程暂停执行

在指定的毫秒数内让当前正在执行的线程休眠。
sleep() 可能会抛出 InterruptedException。因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

join(long millis):指等待t线程一段时间(或终止)

  • join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程(终止)”。
  • 目的
    在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

yield():暂停当前正在执行的线程对象,并执行其他线程。

  • yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。
  • 目的
    使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

wait()

本小节转载自使用wait/notify/notifyAll实现线程间通信的几点重要说明

用于线程间通信

在Java中,可以通过配合调用Object对象的wait()方法和notify()方法或notifyAll()方法来实现线程间的通信

语义

在线程中调用wait()方法,将阻塞等待其他线程的通知(其他线程调用notify()方法或notifyAll()方法),在线程中调用notify()方法或notifyAll()方法,将通知其他线程从wait()方法处返回。

Object是所有类的超类,它有5个方法组成了等待/通知机制的核心:notify()、notifyAll()、wait()、wait(long)和wait(long,int)。在Java中,所有的类都从Object继承而来,因此,所有的类都拥有这些共有方法可供使用。而且,由于他们都被声明为final,因此在子类中不能覆写任何一个方法。

使用中需要注意

  1. wait() --- "我去等待了"
 public final void wait()  throws InterruptedException,IllegalMonitorStateException 
  • 该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。
  • 进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch结构。
  1. notify()---"大家来争我"
public final native void notify() throws IllegalMonitorStateException
  • 该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,的如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException
  • 该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify后,当前线程不会马上释放该对象锁,wait所在的线程并不能马上获取该对象锁,要等到程序退出synchronized代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。这里需要注意:它们等待的是被notify或notifyAll,而不是锁。这与下面的notifyAll()方法执行后的情况不同。
  1. notifyAll()
     public final native void notifyAll() throws IllegalMonitorStateException

该方法与notify()方法的工作方式相同,重要的一点差异是:
notifyAll使所有原来在该对象上wait的线程统统退出wait的状态(即全部被唤醒,不再等待notify或notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll线程退出调用了notifyAll的synchronized代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

  1. wait(long)和wait(long,int)
    显然,这两个方法是设置等待超时时间的,后者在超值时间上加上ns,精度也难以达到,因此,该方法很少使用。对于前者,如果在等待线程接到通知或被中断之前,已经超过了指定的毫秒数,则它通过竞争重新获得锁,并从wait(long)返回。另外,需要知道,如果设置了超时时间,当wait()返回时,我们不能确定它是因为接到了通知还是因为超时而返回的,因为wait()方法不会返回任何相关的信息。但一般可以通过设置标志位来判断,在notify之前改变标志位的值,在wait()方法后读取该标志位的值来判断,当然为了保证notify不被遗漏,我们还需要另外一个标志位来循环判断是否调用wait()方法。
  2. 深入理解:
    如果线程调用了对象的wait()方法,那么线程便会处于该对象的
    等待池中,等待池中的线程不会去竞争该对象的锁。
    当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁
    优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

wait和sleep区别

共同点:

  1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回
  2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。 (不建议使用该方法)
  • 如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
  • 需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。

不同点:

  1. 所属的类
    Thread类的方法:sleep(),yield()等
    Object的方法:wait()和notify()等
  2. 是否释放锁
    每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步
    sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  3. 使用限制
    wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 。

sleep()和yield()的区别

  • sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
  • 另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

三线程打印ABC

要求

java实现三个线程A B C:A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。

方法一.使用synchronized() + wait(),nitify()

  • 题意
    问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC→ThreadA……循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。
  • 思路
    主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就是前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印字母,之后首先调用self.notifyAll()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。

程序运行的主要过程就是
A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B;
线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C;
线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A……

  • 注意
    为了避免JVM启动ThreadA、ThreadB、ThreadC三个线程顺序的不确定性。需要让A,B,C三个线程以确定的顺序启动,中间加一段sleep()确保前一个线程已启动

代码实现

public class ABC {
    public static class ThreadPrinter implements Runnable {
        private String name;
        private Object prev;//前一个对象的锁
        private Object self;//自己的锁

        private ThreadPrinter(String name, Object prev, Object self) {
            this.name = name;
            this.prev = prev;
            this.self = self;
        }

        @Override
        public void run() {
            int count = 10;
            while (count > 0) {// 多线程并发,不能用if,必须用循环测试等待条件,避免虚假唤醒
                synchronized (prev) { // 先获取 prev 锁
                    synchronized (self) {// 再获取 self 锁
                        System.out.print(name);
                        count--;
                        self.notifyAll();// 先释放 self,唤醒其他线程竞争self锁
                    }
                    try {
                        prev.wait(); // 再释放 prev,休眠等待唤醒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();
        ThreadPrinter pa = new ThreadPrinter("A", c, a);
        ThreadPrinter pb = new ThreadPrinter("B", a, b);
        ThreadPrinter pc = new ThreadPrinter("C", b, c);

        new Thread(pa).start();
        Thread.sleep(10);
        new Thread(pb).start();
        Thread.sleep(10);
        new Thread(pc).start();
        Thread.sleep(10);
    }
}

输出

打印结果

方法二. 使用Lock

代码实现

import java.util.concurrent.locks.*;

public class ABC_Lock {
    private static Lock lock = new ReentrantLock();//通过Lock锁来保证线程的访问的互斥
    private static int state = 0;//控制ABC的打印语句是否执行,获取Lock之后的判断
 
    static class ThreadA extends Thread {
        @Override
        public void run() {
            int count = 10;
            while(count > 0) {
                try {
                    lock.lock();
                    while (state % 3 == 0) {//多线程并发,不能用if,必须用循环测试等待条件,避免虚假唤醒
                        System.out.print("A");
                        state++;
                        count--;
                    }
                } finally {
                    lock.unlock();// lock()和unlock()操作结合try/catch使用
                }
            }
        }
    }
 
    static class ThreadB extends Thread {
        @Override
        public void run() {
            int count = 10;
            while(count > 0) {
                try {
                    lock.lock();
                    while (state % 3 == 1) {//多线程并发,不能用if,必须用循环测试等待条件,避免虚假唤醒
                        System.out.print("B");
                        state++;
                        count--;
                    }
                } finally {
                    lock.unlock();// lock()和unlock()操作结合try/catch使用
                }
            }
        }
    }
 
    static class ThreadC extends Thread {
        @Override
        public void run() {
            int count = 10;
            while(count > 0){
                try {
                    lock.lock();
                    while (state % 3 == 2) {//多线程并发,不能用if,必须用循环测试等待条件,避免虚假唤醒
                        System.out.print("C");
                        state++;
                        count--;
                    }
                } finally {
                    lock.unlock();// lock()和unlock()操作结合try/catch使用
                }
            }
        }
    }
 
    public static void main(String[] args) {
        new ThreadA().start();
        new ThreadB().start();
        new ThreadC().start();
    }
}

输出

打印结果

参考文章
并发编程的优缺点
java多线程入门学习(一)
Java多线程学习(吐血超详细总结)
oracle官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350

推荐阅读更多精彩内容

  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,952评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,444评论 1 15
  • 该文章转自:http://blog.csdn.net/evankaka/article/details/44153...
    加来依蓝阅读 7,337评论 3 87
  • Java多线程入门不完全指南 序言 最近在读《把时间当作朋友》,序言就教导我“无论是谁,都是在某一刻意识到时间的珍...
    leo5592368阅读 3,541评论 2 5
  • 又是一夜,睡不着。。
    _S小姐阅读 105评论 0 0