Java多线程

线程是操作系统能够进行运算调度的最小单位,包括在进程中,是进程实际运作单位;线程是进程的子集,一个进程(一个java虚拟机就是一个进程)可以有很多线程,线程共享进程的内存空间,线程拥有独有的虚拟机栈(本地方法栈和虚拟机栈),进程相互之间不共享内存空间。[详情请点击。][]
[详情请点击。]: http://www.java67.com/2012/12/what-is-difference-between-thread-vs-process-java.html


一、Java类实现、运行线程

在语言层面上Java有两种方式,继承java.lang.Thead和直接调用Runable接口来重写run()方法实现线程。

上述两种方法的选择主要根据线程类本身的继承结果,如何选择请点击

要运行Thread,需要使用start()或者run()。前者被用来启动新创建的线程,而且start()内部调用run()方法。注意,直接调用run和调用start完全不同。当你调用run的时候,只会在原线程执行(只是执行run函数体中的内容,run就相当于运行Runable的run)。只有start才会执行新线程。具体可以点击这里

Callable是JDK1.5增加的,不仅能抛出异常,还能返回Future返回值,而Runable则没有这些功能(具体请点击),使用详解见下面代码:

 ExecutorService executorService = Executors.newSingleThreadExecutor();
        try {
            try {
                Thread.sleep(1000);
                try {
                    System.out.println(executorService.submit(new Callable<Integer>() {
                        @Override
                        public Integer call() throws Exception {
                            return 1;
                        }
                    }).get());
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            executorService.shutdown();
        }

二、Java内存模型

JMM(Java Memory Model)是通过各种操作来定义的,包括对变量的读/写操作,监视器的枷锁和释放操作,以及线程启动和合并操作。JMM为所有操作定义了一个偏序关系(Happens-Before)

  • 程序顺序规则:程序中操作A在操作B之前,那么线程中A操作将在B操作之前执行。
  • 监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
  • volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行。
  • 线程启动规则:线程上的start的调用必须在该线程中执行任何操作之前执行。
  • 线程结束规则:线程的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者join中成功返回,或者Thread.isAlive返回false。
  • 中断规则:一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出异常,调用判定规则)。
  • 终结器规则:对象的构造函数必须在启动该对象的终结器之前完成。
  • 传递性:如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么A必须在C之前。

三、终止java线程和线程中发生异常

Java 提供了很丰富的 API 但没有为停止线程提供 API。JDK 1.0 本来有一些像 stop (), suspend () 和 resume ()的控制方法但是由于潜在的死锁威胁因此在后续的 JDK 版本中他们被弃用了,之后 Java API 的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当 run () 或者 call () 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用 volatile 布尔变量来退出 run ()方法的循环或者是取消任务来中断线程。点击这里查看示例代码。
线程运行时发生异常会发生的状况:
很刁钻的 Java 面试题, 简单的说,如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候 JVM 会使用 Thread.getUncaughtExceptionHandler ()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException ()方法进行处理。
notify和notifyall的区别
因为多线程可以等待单监控锁,Java API 的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。notify ()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而 notifyAll ()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。此地博客有更详细的资料和示例代码。
** wait, notify 和 notifyAll 这些方法不在 thread 类里面的原因:**
这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在 Object 类里是有意义的,还有不把它放在 Thread 类里的原因。一个很明显的原因是 JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的 wait ()方法就有意义了。如果 wait ()方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了。简单的说,由于 wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。你也可以查看这篇文章了解更多。

四、多线程辅助变量

竞争条件是线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的 bugs。这种 bugs 很难发现而且会重复出现,因为线程间的随机竞争。一个例子就是无序处理,详见答案
ThreadLocal 是 Java 里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用 ThreadLocal 让 SimpleDateFormat 变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是 ThreadLocalRandom 类,它在多线程环境中减少了创建代价高昂的 Random 对象的个数。查看答案了解更多。

在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是调用了 Runnable 接口所以它可以提交给 Executor 来执行。
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5 介绍了并发集合像 ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。更多内容详见答案
Java 中的 Semaphore 是一种新的同步类,它是一个计数信号。从概念上讲,从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个 acquire (),然后再获取该许可。每个 release ()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。信号量常常用于多线程的代码中,比如数据库连接池。更多详细信息请点击这里

CyclicBarrier和CountDownLatch都可以用来让一组线程等待其他线程。与CyclicBarrier不同的是,CountdownLatch不能重新使用。下面是CyclicBarrier的样例:

public class CyclicBarrierTest {
    private static final int THREAD_NUM = 10;
    public static class WorkThred implements Runnable{
        final CyclicBarrier barrier ;

        public WorkThred(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        @Override
        public void run() {
            System.out.println("awiat:"+Thread.currentThread());
            try {
                barrier.await();
                System.out.println("working:"+Thread.currentThread());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }

        }
    }
    public static void main(String[] args) {
        final CyclicBarrier cb = new CyclicBarrier(THREAD_NUM, new Runnable() {
            @Override
            public void run() {
                System.out.println("condition filled.");
            }
        });
        for (int i=0;i<THREAD_NUM;i++){
            new Thread(new WorkThred(cb)).start();
        }
    }
}
output:
awiat:Thread[Thread-0,5,main]
awiat:Thread[Thread-1,5,main]
awiat:Thread[Thread-2,5,main]
condition filled.
working:Thread[Thread-2,5,main]
working:Thread[Thread-0,5,main]
working:Thread[Thread-1,5,main]

其中如果你将创建的线程数目double,会发现之后的三个新线程会按照上述输出相同的形式输出,即Barrier是一个永远在的条件,只要满足就会进行下一步。最后reset方法适用于恢复barrier变量的等待条件到初始值,如果还有线程在等待barrier,那么就会抛出BrokenBarrierException 。
Java语言规范要求JVM在线程中维护一种类似串行的语义:只要程序的最终结果与严格串行环境中执行的结果相同,那么指令重新排序是允许的、变量写入寄存器不写入内存、并行执行代码、缓存提交到内存的顺序不同还有本地缓存中的值对于其他处理器不可见。这都导致值的不可见性。

volatile 是一个特殊的修饰符,只有成员变量才能使用它。在 Java 并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile 变量可以保证下一个读取操作会在前一个写操作之后发生,就是上一题的 volatile 变量规则。点击这里查看更多 volatile 的相关内容。votatile保证变量原子性(long、double等)。
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”——《深入理解JVM》

五、Java锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:

  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

乐观锁和悲观锁的作用:

  • 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
  • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

活锁:活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。

检查一个线程是否拥有锁:在 java.lang.Thread 中有一个方法叫 holdsLock (),它返回 true 如果当且仅当当前线程拥有某个具体对象的锁。你可以查看这篇文章了解更多。
Java 在过去很长一段时间只能通过** synchronized** 关键字来实现互斥,它有一些缺点。比如你不能扩展锁之外的方法或者块边界,尝试获取锁时不能中途取消等。Java 5 通过 Lock 接口提供了更复杂的控制来解决这些问题。 ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义且它还具有可扩展性。你可以查看这篇文章了解更多。
一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。Java 中的 ReadWriteLock 是 Java 5 中新增的一个接口,一个 ReadWriteLock 维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用 JDK 中的 ReentrantReadWriteLock 来实现这个规则,它最多支持 65535 个写锁和 65535 个读锁。

六、线程状态及其转换

一共有如下五种状态:

  • 新建:创建后尚未启动的线程。
  • 运行:其包括了操作系统的Running和Ready状态,此时有可能正在执行,也有可能正在等待cpu分配时间。
  • 无限期等待(waiting):他们需要等待被唤醒。使用Object.wait()、Thread.join()、LockSupport.park()方法可以进入这个状态。
  • Timed Waiting:不会分配CPU,一旦时间达到,自己会由系统自动唤醒。
  • 阻塞:阻塞事件和等待事件不同的是,阻塞状态再等待获取一个排他锁,这个动作将会在另外一个线程放弃这个锁的时候发生;等待则是等待一段时间,或者唤醒动作的发生。线程进入同步区域时,线程将进入这个状态。
  • 结束:已经终止线程的线程状态。

Java 程序中 wait 和 sleep 都会造成某种形式的暂停,它们可以满足不同的需要。wait ()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而 sleep ()方法仅仅释放 CPU 资源或者让当前线程停止执行一段时间,但不会释放锁。你可以查看这篇文章获得更多信息。
同步块内的线程抛出异常。无论你的同步块是正常还是异常退出的,里面的线程都会释放锁,所以对比锁接口我更喜欢同步块,因为它不用我花费精力去释放锁,该功能可以在 finally block 里释放锁实现。
如果你提交任务时,线程池队列已满,许多程序员会认为该任务会阻塞直到线程池队列有空位。事实上如果一个任务不能被调度执行那么 ThreadPoolExecutor’s submit ()方法将会抛出一个 RejectedExecutionException 异常。

7、fork/Join框架

此框架主要是用于切割大任务成子任务,并保证结果按照指定顺序发生。

fork/join

一个fork/join框架之下的任务由ForkJoinTask类表示。ForkJoinTask实现了Future接口,可以按照Future接口的方式来使用。在ForkJoinTask类中之重要的两个方法fork和join。fork方法用以一部方式启动任务的执行,join方法则等待任务完成并返回指向结果。在创建自己的任务是,最好不要直接继承自ForkJoinTask类,而要继承自ForkJoinTask类的子类RecurisiveTask或RecurisiveAction类。两种的区别在于RecurisiveTask类表示的任务可以返回结果,而RecurisiveAction类不行。
简单总结:ForkJoin主要提供了两个主要的执行任务的接口。RecurisiveAction与RecurisiveTask 。

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

推荐阅读更多精彩内容