Java多线程编程
Java线程的创建和启动
继承Thread类创建线程类
- 继承Thread类,并重写run()方法,该run()方法的方法体代表线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即线程对象。
- 调用线程对象的start()来启动线程。
1 | public class FirstThread extends Thread |
Java程序运行时默认的主线程总共是main()方法的方法体。
- Thread.currentThread(), Thread类的静态方法,总是返回当前正在执行的线程对象。
- getName(): Thread类的实例方法,返回调用该方法的线程名字。
使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。
实现Runnable接口创建线程类
- 定义Runnable接口的实现类,重写run()方法。
- 创建Runnable实现类的实例,并依次实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
1 | public class SecondThread implements Runnable |
程序所创建的Runnable对象只是线程的target,多个线程可共享一个target,所以可以共享这个实例的实例变量。
使用Callable和Future创建线程
类似与Runnable接口的用法,Callable接口提供了一个call()方法作为线程执行体,不同的是,call()可以有返回值。,且可以声明抛出异常。
Future接口代表Callable接口call()方法的返回值,并为接口提供了一个FutureTask的实现了,该实现类实现了Future接口,实现了Runnable接口—可以作为Thread类的target。
几个方法:
- boolean cancle(boolean mayInterruptIfRunning)
- V get(): 返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
- V get(long timeout, TimeUnit unit)
- boolean isCancelled()
- boolean isDone()
步骤:
- 创建Callable接口实现类,并实现call()方法
- 使用FutureTask类包装Callable对象
- 使用FutureTask对象作为Thread对象的target创建并启动线程
- 通过调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
1 | public class ThirdThread implements Callable<Integer> |
线程的生命周期
新建-就绪-运行—(就绪)-运行-阻塞-(就绪)-死亡
新建和就绪状态
当程序使用new关键字创建了一个线程后,该线程就处于新建状态。
只有当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,但并没有直接运行,只是可以运行了。至于何时运行,取决于线程调度器的调度。
启动线程要使用start()方法,不是run()方法,否则只是一个普通的方法调用!
运行和阻塞状态
现代操作系统都采用抢占式调度策略,也就是系统会给每个可执行的线程一个小时间片段来处理任务,该时间段用完后,系统就会剥夺该线程所占用的资源,将其放入就绪状态,然其他线程获得执行机会。如果线程执行时被阻塞,系统也会将资源重新给其他线程。
发生如下情况时,线程将会进入阻塞状态:
- 线程调用sleep()方法主动放弃占用的处理器资源
- 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞。
- 线程试图获得一个同步监视器,但该监视器正在被其他线程所持有。
- 线程在等待某个notify
- 程序调用了线程的suspend()将其挂起。
当前正在执行的线程被阻塞后,其他线程就可以获得执行的机会。被阻塞的线程会在阻塞基础后重新进入就绪状态,等待再次被调用。
针对上面几种情况,下面情况可以解除上面的阻塞:
- sleep()方法的时间超过
- 线程调用的阻塞式IO方法已经返回。
- 线程成功获得试图取得的同步监视器
- 获得notify
- 挂起的线程被调用了resume()恢复方法。
注意yield()方法是直接放弃资源重新进入就绪队列。
线程死亡
几种情况:
- run()或call()方法执行完成。
- 线程抛出一个未捕获的Exception或Error。
- 直接调用该线程的stop()方法。
当主线程结束时,其他线程不收任何影响,并不会随之结束。一旦子线程启动起来,就拥有和主线程相同的地位,不会受主线程的影响。
isAlive()
,新建,死亡时返回true,其他返回false。
不能再start已经启动并死亡的线程。
控制线程
join线程
Thread提供了让一个线程等待另一个线程执行完的方法—join(). 当某个程序执行流中调用其他线程的join()方法时,调用线程即被阻塞,直到被join()方法加入的join线程执行完为止.
通常有使用线程的程序调用,将大问题划分为许多小问题,每个小问题分配一个线程。
- join()
- join(long millis)
- join(long millis, int nanos)
1 | public class JoinThread extends Thread |
后台进程
后台线程的任务是为其他线程提供服务。如果所有前台线程都死亡,后台线程就会自动死亡。
必须要在启动前设置。
- setDaemon(true)
- isDaemon()
1 | public class DaemonThread extends Thread |
线程睡眠:sleep
让当前正在执行的线程暂停一段时间,并进入阻塞状态。
- static void sleep(long millis)
- static void sleep(long millis, int nanos)
1 | public class SleepTest |
线程让步:yield
放弃资源,但不会进入阻塞状态,有可能马上又得到调用。
1 | public class YieldTest extends Thread |
改变线程优先级
setPriority(int newPriority)
getPriotiry()
优先级为1-10之间,最好使用下面几个静态常量
- MAX_PRIORITY: 10
- MIN_PRIORITY: 0
- NORM_PRIORITY: 5
高优先级的线程将会获得更多的执行机会。
1 | public class PriorityTest extends Thread |
线程同步
由于线程调度的随机性,当多个线程需要访问并修改同一个数据时,很容易出现线程安全问题。
同步代码块
1 | synchronized(obj) { |
obj就是同步监视器,线程在执行同步代码块之前,必须先获得对同步监视器的锁定。
任何时刻只能有一个线程可以获得对同步监视器的锁定,执行完成后,则会释放该同步监视器的锁定。
1 | public class DrawThread extends Thread |
任何时候只有一个线程可以进入修改共享资源的代码区(临界区)。
同步方法
对于synchronized修饰的实例方法(非static方法),无需显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
使用同步方法可以非常方便的实现线程安全的类,该类的对象可以被多个线程安全地访问。
1 | public class Account |
synchronized可以修饰代码块,方法,但不能修饰变量,构造器
释放同步监视器
无法显式释放,下面几种情况会释放对同步监视器的锁定:
- 当前线程同步方法,同步代码块执行结束
- 当前线程在同步方法,同步代码中遇到break,retrun终止了该代码块,该方法继续执行
- 当前线程在同步方法,同步代码出现了未处理的Error或Exception
- 当前线程在同步方法,同步代码中调用了同步检索器的wait()方法,则当前线程暂停,释放同步监视器。
下面情况不会释放:
- 当前线程调用Thread.sleep(), Thread.yield()方法来暂停当前线程的执行
- 其他线程调用了该线程的suspend()方法将该线程挂起。
同步锁(Lock)
通过显式定义同步锁对象来实现同步,这种机制下,同步锁由Lock对象充当。
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock,ReadWriteLock是Java 5的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。
比较常用的是ReentrantLock。可以显式的加锁,释放锁。
1 | public class Account |
对应一个Account对象,同一时刻只能有一个线程进入临界区。
当获取了多个锁时,必须已相反的顺序释放,且必须在所有锁被获取的相同的范围内释放所有锁。
ReetrantLock锁具有可重入性,也就是说一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用。
死锁
两个线程互相等待对方释放同步监视器就会发生死锁。
1 | class A |
线程通信
当线程在系统内运行时,线程调度具有一定的透明性,程序通常无法准确控制线程的轮换执行。但Java也提供了一些机制来保证线程协调运行。
传统的线程通信
Oject类提供了wait(), notify(), notifyAll()三个方法。这三个方法必须由同步监视器对象来调用,这可分为一下两种情况。
- synchroninzed, 因为该类默认实例(this)就是同步监视器,所以可以在同步方法直接调用这三个方法。
对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这个三个方法(只适用于Synchronized同步的线程)。
wait(), 导致当前线程释放同步监视器并阻塞等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。可以带时间参数。
- notify(): 唤醒在此同步监视器上等待的一个线程,如果有多个线程在等待(调用了wait方法的线程),则会随机选择唤醒其中一个线程,重新进入尝试获得锁的队列。
- notiifyAll(): 唤醒在此同步监视器等待的所有线程(调用了wait方法的线程),重新进入尝试获得锁的队列。
1 | public class Account |
使用Condition控制线程通信
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则不存在隐式的同步监视器,也就是不能用上面的方法进行线程通信了。
当使用Lock对象来保证同步时,Java提供了一个Condition类,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
Condition实例被绑定在一个Lock对象上,可以通过调用Lock对象的newCondition()方法即可。Condition类提供了如下三个方法:
- await(): 类似与隐式同步监视器上的wait()方法,导致当前线程释放锁并等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。
- signal(): 唤醒在此lock对象上等待的单个线程(获得锁又通过调用await()的放弃锁的线程)。如果有多个线程,则随机选择一个。
- signalAll(): 唤醒在此lock对象上等待的所有线程(获得锁又通过调用await()的放弃锁的线程)。
1 | public class Account |
使用阻塞队列(BlockingQueue)控制线程通信
BlockingQueue的主要用途是作为线程同步的工具,当生产者线程试图向BlockingQueue中放入元素时,如果队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程被阻塞。
- put(E e)
- take()
1 | class Producer extends Thread |