java多线程——线程创建、控制、同步、通信、线程组和未处理异常、线程池

多线程——线程创建和启动

进程

当一个程序进入内存中运行时,即变成一个进程,其具有一定的独立功能,时系统进行资源分配和调度一个独立单位。

线程

也被称为轻量级进程,线程是进程的执行单元,如同进程在系统中的地位一样,线程在进程中是独立的、并发的执行流。一个进程可以有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、程序计数器和自己的局部变量,但不拥有系统资源。因为多个线程共享父进程的全部资源。线程也可以创建和撤销另一个线程,同时线程之间的执行时抢占式的,也就是说当前运行的线程可能被挂起,以便另一个线程执行。

 系统创建一个进程时,需为其分配独立的内存空间和相关的资源,但是创建一个线程则简单得多,因此用线程实现来实现并发比多进程实现并发性能要高得多。

 

线程创建和启动

1. 继承Thread类创建线程类

继承Thread类,并在子类中重写public void run ()方法,这个方法就是线程执行体。创建了Thread子类实例就创建了线程对象。通过调用对象的start()方法启动线程。
通过调用Thread静态方法currentThread()可以获得当前线程对象。线程对象调用getName()可以获得线程名称。

public class MyThread extends Thread{

    @Override
    public void run() {
        /*
        线程主体
         */
    }

    public static void main(String[] args) {
        System.out.println("Hello World!");
        Thread myThread = new MyThread();
        myThread.start();
    }
}

2. 实现Runnable接口创建线程类
定义Runnable接口实现类,并重写run()方法,这个方法同样是线程的执行体。创建Runnable实例,作为Thread的target来创建Thread对象,此Thread对象即为创建的线程。

public class SecondThread implements Runnable {

    @Override
    public void run() {
        /*
        线程主体
         */
    }

    public static void main(String[] args) {

        Runnable runnable = new SecondThread();

        // runnable作为线程target
        Thread secondThread = new Thread(runable);

        secondThread.start();
    }
}

 

3.使用Callable和Future创建线程(可以获得线程返回结果,可以抛出异常)

创建Callable接口的实现类,并实现call()方法,该call()方法作为线程执行体,且该call()方法有返回值,再创建Callable接口的实例。可以直接使用Lamda表达式创建Callable对象。

使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

使用FutureTask对象作为Thread对象的taget创建并启动新线程。

调用FutrueTask对象的get()方法来获得子线程执行结束后的返回值。

public class ThirdThread extends Thread{
    public static void main(String[] args) {
        // 创建Callable对象,使用Lambda表达式创建callable对象
        // 泛型指定的是call()方法的返回值
        // 使用FutureTask来包装callable对象

        // 函数式接口和Lambda表达式可以隐式转换
        FutureTask<Integer> task = new FutureTask<>( ()->{// 通过函数式接口实现Callable
            int i = 0;
            return i;
        } );
        new Thread(task, "有返回值的线程").start();
    }
}

对比:Runnable和Callable创建的多线程类可以继承其他类,同时这两种方法可以实现多个线程对象共享一个target,从而可以让多个线程处理一个事物,实现cpu、代码、数据分开。直接继承Thread类创建的对象,编码较简单,但是无法继承其他类。

线程的生命周期

1.新建

使用new关键字创建了线程之后,线程即处于新建状态。和其他java对象一样,仅由jvm为其分配内存,而不表现出任何的动态的特征。

2.就绪

当线程创建之后,调用了start()方法,该线程即处于就绪状态,java虚拟机会为其创建方法调用栈和程序计数器,但线程并没有开始运行,只是表示线程可以运行了。而何时开始运行取决于JVM里的线程调度器。(但是绝不可以直接调用run方法运行线程执行体,这种调用方法,系统不会将其作为线程来处理,只会视为普通方法调用)。

3.运行和阻塞状态

当线程开始执行run方法的线程执行体,则处于运行状态。但是当线程数量多于处理器数量时,会存在多个线程在一个CPU上轮换的现象,当线程处于挂起没有cpu处理时,即进入阻塞状态。阻塞的情况有:

1)在线程调用sleep()方法主动放弃所占用的处理器资源会阻塞;2)调用了一个阻塞式的IO方法,并在该方法返回前线程被阻塞;3)线程试图获得一个正被其他线程持有的同步监视器(在线程同步博文中讲到)会阻塞;4)线程在等待某个通知(notify,在线程通信博客中提及);5)程序调用suspend()方法将该线程挂起,但这个方法容易导致死锁,应该尽量避免。

针对上面对应的阻塞,当出现如下情况时,线程重新进入就绪状态:

1)调用sleep()过了指定时间;2)阻塞式IO方法已经返回;3)线程成功地获取了同步监视器;4)线程得到了其他线程发起的通知;5)处于挂起状态的线程被调用了resume()方法恢复。

4.线程死亡

以下几种情况会让线程结束:1)当线程run()或call()方法执行完成;2)线程抛出一个未捕获的Exception、Error;3)直接调用该线程的stop()方法,线程结束即处于死亡状态。

多线程——线程控制

java的线程支持提供了一些便捷的工具方法,通过这些方法可以很好地控制线程执行。

join线程

在某个线程中调用其他线程的join()方法,调用线程会阻塞等待被调用join()的线程执行完。通常这种用法用于在某个父线程中,开启多个子线程去完成多个子任务,父线程等待子线程任务都完成,再来进行下一步的处理。

join()方法的三种重载形式:

1)join():等待被join的线程执行完;

2)join(long millis):等待millis毫秒后,等待线程还没执行完,则不继续等待。

3)join(long millis, long nanos):等待线程执行时间最长为millis毫秒nanos微秒。

后台线程

有一种线程是在后台运行,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程”,JVM的垃圾回收线程就是典型的后台线程。

特征:如果前台线程都死亡,后台线程会自动死亡。前台线程子线程默认是后台线程,后台线程子线程默认是后台线程。

setDaemon():可以通过调用Thread对象的setDaemon(true)使得线程为后台线程。需要在启动之前设置否则会引发IllegalThreadStateException异常。

isDaemon():Thread对象的方法可以判断该线程是否是后台线程。

线程睡眠sleep

Thread类的静态方法,声明抛出InterruptedException,可以让线程暂停一段时间并进入阻塞状态。其有两种重载方法:

static void sleep(long milis):暂停线程millis毫秒,并进入阻塞状态。

static void sleep(long millis, long nanos):暂停线程millis毫秒,nanos微秒,并进入阻塞状态。

线程让步yield

yield()也是Thread类的静态方法,它的作用是将线程暂停,并进入就绪状态,这一点是跟睡眠sleep不同的。所以,线程调度器也有可能在线程yield让步后,又立马将其调出来重新执行。

改变线程优先级

Thread类提供了setPriority(int newPriority),getPriority()方法来设置和返回指定线程的优先级,setPriority的参数可以是一个范围1~10之间的整数,也可以是三个静态常量MAX_PRIORITY(10),MIN_PRIORITY(1),NORM_PRIORITY(5)。

多线程——线程同步

线程之间会公用一些资源,当多个线程访问同一个数据时,很容易造成意想不到的问题。为了解决这个问题,java的多线程提供了几种方法。

同步代码块

synchronized(obj)
{
    ...
    // 同步代码块
}

synchronized括号内obj就是代码监视器,线程开始执行同步代码块之前,需要先获得对同步监视器的锁定。同步代码执行完成后,线程会释放对该同步监视器的锁定。

java允许使用任何对象作为同步监视器,但是出于同步监视器的目的,因此通常可以使用可能被并发访问的共享资源充当同步资源器。

同步方法

sychronized还可以用来修饰某个方法,这个方法就称为同步方法,对于这个方法,无需显示指定同步监视器,同步监视器就是this,也即调用该方法的对象。synchronized关键词不能用来修饰构造方法和成员变量。

public synchronized void function() {
    // 方法内容
}

释放同步监视器的锁定

线程在如下几种情况会释放对同步监视器的锁定:

1.同步方法、同步代码块执行结束,当前线程即释放同步监视器。

2.当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。

3.当前线程在同步代码块出现了未处理的Error或Exception,导致了该代码块、该同步方法异常结束,当前线程会释放同步监视器。

4.当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。

线程在如下情况下不会释放同步监视器:

1.线程执行同步代码块或方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,但当前线程不会释放同步监视器。

2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法,将该线程挂起,但是该线程不会释放同步监视器。

同步锁(Lock)

从java5开始,java提供了一个功能更强大的线程同步机制,通过显示定义同步锁对象来实现同步,而不是系统隐式使用同步监视器。java5中为同步锁提供了两个根接口,Lock、ReadWriteLock,ReentrantLock(可重入锁)是对Lock的实现类,ReentrantReadWriteLock式ReadWirteLock的实现类。同步锁比较常用的方法如下:

class X {
    // 定义同步锁
    private final Lock lock = new ReentrantLock();

    // 方法中使用同步锁
    public void funtion() {
        // 加锁
        lock.lock();
        try {
            // 方法内容
        }
        catch(...) {
        }
        finally {
            // 解锁
            lock.unlock();
        }
    }

}

死锁

当两个线程都在等待对方释放同步监视器时,就会造成死锁,程序无法继续执行。因此线程间要避免造成死锁。

多线程——线程通信

在线程运行时,程序无法准确控制线程,但是java提供了一些机制来保证线程协调运行。

传统的线程通信

Object类提供了wait()、notify()和notifyAll()三个方法,这个并不属于Thread类,必须由同步监视器对象来调用,分为两种情况:

1.synchronized修饰的同步方法,由于同步监视器是该方法所属类的实例this,故可以直接在方法中调用wait、notify、notifyAll方法。

2.synchronized同步代码块,监视器是括号中的对象,故需由此对象调用。

  • wait():导致该线程等待并释放同步监视器,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。wait方法有三种重载方法,不带参数(等待直到notify或notifyAll唤醒)、带毫秒参数、带毫秒和微秒参数。
  • notify():当线程调用同步监视器的notify方法,会随机唤醒一个wait中的线程,但是只有在当前线程释放同步监视器后,唤醒的线程才有机会执行。
  • notifyAll():唤醒同步监视器上所有wait等待的线程,但同样当前线程并不会释放同步监视器。

使用condition来控制线程通讯

当线程使用Lock对象来保证同步时,由于没有隐式的同步监视器,故不能使用传统的方法。Java提供了Condition可以让那些已经得到Lock对象的线程无法继续执行并释放Lock对象。同时,Condition也可以唤醒其他处于等待的线程。

要获取绑定在Lock对象上的Condition对象,可以直接调用该Lock对象的newCondition()方法即可。

// 获取Lock对象
Lock lock = new ReentrantLock();

// 获取与lock绑定的Condition对象
Condition codition = lock.newCondition();

/**Condition对象的方法与功能**/

// 停止当前线程直到其他线程调用该Condition对象的signal或signalAll的方法,还有很多变体提供丰富的功能
await();

// 唤醒已停止的一个线程
signal();

// 唤醒所有停止的线程
signalAll();

使用阻塞队列

BlockingQueue是Queue的子接口,但是并不是用作容器,而是用作线程同步的工具。其特征是:当生产者线程试图想BlockingQueue中放入元素时,如果该队列已经满了,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果队列为空,则该线程被阻塞。

阻塞队列有几套方法配合使用:

BlockingQueue方法与对应关系
     
队尾插入元素

add(e)

可抛异常、可阻塞

offer(e)

可抛异常、可阻塞

put(e)

可抛异常、可阻塞

offer(e,time,unit)

可抛异常、可阻塞、可指定超时时长

队头删除元素

remove()

可抛异常、可阻塞

poll()

可抛异常、可阻塞

take()

可抛异常、可阻塞

poll(time,unit)

可抛异常、可阻塞、可指定超时时长

获取、不删除元素

element()

可抛异常

peek()

可抛异常

 

多线程——线程组和未处理的异常

java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,如果没有显示制定线程所属的线程组,则该线程归属于默认线程组。同时,在默认情况下,子线程与创建它的线程归属于同一线程组,一旦一个线程加入某个线程组后,该线程一直属于这个线程组,运行中不能改变,直至线程死亡。

为线程设置线程组

Thread类提供了几个构造器来设置新创建的线程属于哪个线程组:

Thread(ThreadGroup group, Runnable target)

Thread(ThreadGroup group, Runnable target, String name)

Thread(ThreadGroup group, String name)

创建线程组对象

同时提供了getThreadGroup()方法获取线程所属的线程组。线程组类提供了两个简单的构造方法来创建实例:

ThreadGroup(String name):指定线程组名字来创建线程组对象

ThreadGroup(ThreadGroup parent, String name):可以指定父线程组和线程组名称

线程组对象ThreadGroup操作线程组内线程:

int activeCount():返回此线程组中活动线程的数目

interrupt():中断线程组中所有线程

isDaemon():判断线程组是否是后台线程组

setDaemon(boolean daemon):将线程组设置为后台线程组,当线程组最后一个线程执行执行结束或被销毁,则后台线程组将自动销毁

setMaxPriority(int pri):设置后台线程组的最高优先级

线程组异常处理

线程组ThreadGroup内还定义了一个很有用的方法void uncaughtException(Thread t, Throwable e),该方法可以处理该线程组内的任意线程所抛出的未处理异常。

从java5开始,加强了线程的异常处理,在线程跑出异常时,jvm会自动查找是否有对应的Thread.UncaughtExceptionHandler对象来处理这个异常,此时会调用uncaughtException(Thread t, Throwable e)方法来处理该异常。UncaughtExceptionHandler是Thread类内部的静态接口,只有一个方法void uncaughtException(Thread t, Throwable e)。Thread类提供了两个方法来设置这个接口:

static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):通过静态方法为Thread类的所有实例设置默认异常处理器。

setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):对象调用方法指定异常处理器。

线程组实现了Thread.UncaughtExceptionHandler接口,故线程组可作为组内的默认异常处理器。当线程抛出未处理异常,处理器调用流程如下:

1.如果该线程通过setUncaughtExceptionHandler制定了处理器,则通过这个处理。

2.否则调用线程组的线程处理器。

3.否则调用线程类的默认处理器。

4.将异常跟踪栈的信息打印到system.sys的错误输出流。如果异常对象是ThreadDeath则不打印。

注意:异常处理器与catch不同之处在于,uncaughtExceptionHandler处理完后还是会将异常抛出到调用上级。

多线程——线程池

系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互,在需要创建大量生存期很短的线程时,更应该考虑启动新线程的性能成本。因此,在这种情况下应该使用线程池,线程池在系统启动时就创建了大量空闲线程,程序将一个Runnable或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run或call方法,当线程执行完后,并不会死亡,而是再次返回线程池中,成为空闲状态,等待执行下一个Runnable对象的run或call方法。

Java8线程池

java8提供了内建线程池,新增了一个Executors工厂类来产生线程池对象,该工厂的几个静态方法可以提供相应的线程池:

public static ExecutorService newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程会缓存在线程池。

public static ExecutorService newFixedThreadPool(int nThreads):创建指定数量线程的线程池。

public static ExecutorService newSingleThreadExecutor():创建一个线程的线程池。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):指定线程数量,同时在指定延迟后执行线程任务,其返回的对象是ScheduledExecutorService是ExecutorService线程池类的子类。

public static ScheduledExecutorService newSingleThreadScheduledExecutor():产生可延迟执行的单线程线程池。

public static ExecutorService newWorkStealingPool(int parallelism):创建持有足够的线程线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。生成的线程池是后台线程池,当所有前台线程都死亡了,线程池中线程会自动死亡。

public static ExecutorService newWorkSteallingPool():上一个方法的简化方法,如果当前机器有n个cpu,则目标并行级别被设置为n。生成的线程池也是后台线程池。

在线程池对象ExecutorService中,提供了三个方法来提交Runnable和Callable任务。

  • Future<?> submit(Runnable task):提交Runnable,由于Runnable是没有返回值的,所以Future获取的返回值是null。但是可以调用Future对象的isDone()、isCancelled()方法来获得Runnable对象的执行状态。
  • <T> Future<T> submit(Runnable task, T result):提交runnable任务,同时显示地将result作为线程执行完后的返回结果。
  • <T> Future<T> submit(Callable<T> task):提交Callable任务给线程池。

在用完了线程池后,调用shutdown()之后不再接受任务,同时将正在执行的任务和已经提交并在等待执行的任务完成,就销毁线程。执行shudonwNow()则会立即停止所有线程的活动任务,同时暂停处理正在等待的任务,返回等待执行的任务列表。

Java8增强的ForkJoinPool

现在的处理器大多是多核心的,如果将一个任务拆分成多个小任务,并分发到这些核心上并行执行,最后合并结果,可以充分利用cpu的计算能力。ForkJoinPool就提供了这样的功能,其提供了两个构造器:

ForkJoinPool(int parallelism):提供parallelism个并行线程的线程池。

ForkJoinPool():提供Runtime.availableProcessors()方法的返回值作为并行线程池数量。

有了ForkJoinPool实例后,可以调用方法submit(ForkJoinTask task)或invoke(ForkJoinTask task)来执行指定任务。其中ForkJoinTask代表可以并行、合并的任务。ForkJoinTask作为抽象类有两个抽象子类RecursiveTask(有返回值)和RecursiveAction(无返回值)。

class RecursiveActionObject extends RecursiveAction {
    // 重写comput方法
    @Override
    protected void compute() {
        // 声明第一个分解对象
        RecursiveActionObject object1 = new RecursiveActionObject();

        // 声明第二个分解对象
        RecursiveActionObject object2 = new RecursiveActionObject();
        
        // 将分解任务并发运行
        object1.fork();
        object2.fork();

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值