多线程,又称之为并发(Concurrency),与并行的意义是不一样的。并行可以理解为两台电脑
同时工作,而并发则是在一台电脑中,通过操作系统完成线程的转换,让两个任务在宏观上是同时进行的。多线程是通过提高系统资源的使用率来提高效率的。
大多数语言都支持多线程编程。本章就从Java的角度介绍一下Java中多线程的历史,多线程的创建、管理,以及资源同步等问题。
1. Java多线程历史
Java多线程自1996年的JDK1.0版本开始,就确定了基础的线程模型——抢占式模型。并且在JDK的后续迭代中未发生实质性的变化。随着JDK版本不断更新,内容也不断增多,在JDK5.0之前,如果编写一个并发程序,我们会创建一些Runnable对象,然后将其交给新创建的Thread对象去执行,这样就需要实现所有与Thread相关的代码,比如说线程的创建,结束以及结果的获取;同样的需要为每一个任务创建一个新的线程,当任务很多时,会严重影响程序的处理能力,负责线程上下文切换的操作系统也会变得超负荷工作。好在JDK5.0为我们提供了执行器框架(Executor Framework),我们就可以通过 Executor 来创建newFixedThreadPool newCachedThreadPool 、newSingleThreadExecutor ScheduledExecutorService 等线程池 用于优化线程的性能。有了执行器框架之后我们只需实现Runnable接口的对象,然后将这个对象交给执行器,它会自动创建所需的线程,来负责Runnable对象的创建,实例化和运行。同时JDK5.0也引入了有返回值的线程Callable接口。JDK6.0提供了线程流程控制类,CyclicBarrier、CountDownLatch,这两个类可以保证多个任务在并行执行都完成的情况下,再执行下一步操作。JDK7.0则提供了fork-join框架,进一步完善了并发流程控制的功能,同时还提供了将一个任务分解成不同子任务的类——Phaser。JDK8.0提供了更容易获取线程执行结果的CompletableFuture类,它是Future的升级版,CompletableFuture可以和Future一样,阻塞等待返回结果,也可以异步等待返回结果。JDK9.0听说改善了锁争用机制,但是还没有研究,这里就先不提。
2. Java多线程状态
线程有五种状态,分别是新建,可运行,运行,阻塞,死亡。下边就分别说一下这几种状态,以
及状态之间是如何转换的。
新建(NEW) :是通过new创建一个线程实例,线程进入新建状态。
可运行(RUNNABLE) :线程实例对象通过调用start()方法,进入可运行状态。为什么说调用start()方法之后只是进入可运行状态,而不是直接进入运行态呢。这个是因为线程是否执行是由操作系统决定的,只有操作系统的调度程序选到这个线程的时候,才会进入运行态。
新建态通过start()方法可以到可运行态。阻塞态线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,线程拿到对象锁,这些线程也将进入可运行状态。运行态线程时间片用完了,调用当前线程的yield()方法,当前线程进入可运行状态。锁池里的线程拿到对象锁后,进入可运行状态。
运行(RUNNING) :可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。线程进入运行态只有一种方式,即线程调度程序从可运行池中选择一个线程执行程序代码。
阻塞(BLOCkED) :阻塞态是指线程因为某些原因放弃cpu使用权,同时让出cpu时间片。处在阻塞态的线程只有再次进入可运行态,才有再次被操作系统选中,运行的可能。
当前线程调用Thread.sleep()方法,当前线程进入阻塞状态。运行在当前线程里的其它线程调用join()方法,当前线程进入阻塞状态。等待用户输入的时候,当前线程进入阻塞状态。
死亡(DEAD) :线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
2.1 代码中处理线程状态注意事项
以实现Runnable接口为例。
//线程类
public class ThreadSummary implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
//调用线程的类
public class SummaryMain {
public static void main(String[] args) {
}
}
通过start方法进入可运行态:
第一点要注意的是调用 run 方法并不可以运行线程,不管是通过Thread调用,还是通过线程对象调用。如下这两种方法都是在执行被调用类里边的方法,并不是启动一个线程。
ThreadSummary summary = new ThreadSummary();
//线程类对象直接调用run方法
summary.run();
Thread t = new Thread(summary);
//通过Thread对象调用run方法
t.run();
有人会问第一个容易理解,是线程对象调用了自身的方法,但是第二个t.run()明明是通过Thread类调用的,为什么也没有启动一个线程呢。我们看一下Thread中的run方法。有个大大的 @Override 注解,而且Thread类是实现了Runnable的,说明run方法就是Runnable里边的run所以说第一种写法和第二种写法本质上是一样的。
@Override
public void run() {
if (target != null) {
target.run();
}
}
所以正确启动一个线程的方法应该是调用Thread的start方法。我们可以看一下start的源码。可以注意到方法里边调用了 start0() 它负责了启动线程,申请堆栈内存,运行run方法,修改线程状态等职责。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
线程优先级使用三个
java中线程的优先级是1~10,1是最低,10是最高,但是运行的时候,优先级为10的线程可能在9的之后运行,这个是因为线程并不是按照优先级的顺序从高到底依次执行的,优先级只是表明线程抢占cpu的可能性,优先级越高,获得cpu的可能越大。而且优先级的差别越大,运行机会差别越明显。
在Java的Thread类中只给出了三个优先级,MIN_PRIORITY MAX_PRIORITY NORM_PRIORITY
线程结束不适用stop
因为使用stop是无法确定在什么时候结束线程的。会使得子线程的逻辑不完整。如果想结束子线程,可以使用自定义标志位,如果是用的ThreadPoolExecutor等线程池,可以使用其自带的shutdown方法。
如下代码,在线程结束之前,我们肯定是想要输出”this is a stop test”的内容的,可是因为调用了stop方法,代码没有完整执行完就结束了。
private static void notUseStop() {
Thread t = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("this is a stop test");
}
};
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.stop();
}
更重要的是stop会破坏原子逻辑—jdk版本是JDK8
如下代码,因为线程类中使用了synchronized关键字,说明synchronized代码块里边是一个原子操作,那么不管怎么操作最终输出0才是我们想要的结果。但是结果输出的是1,并不是0。这个就是因为调用了Thread的stop方法,它丢弃了所有的锁,破坏了原子性。
//调用线程的方法
private static void notUseStop() {
NotUseStop notUseStop = new NotUseStop();
Thread t = new Thread(notUseStop);
t.start();
for (int i = 0; i < 5; i++) {
new Thread(notUseStop).start();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.stop(); 为第三方
}
//线程类
class NotUseStop implements Runnable {
private int a = 0;
@Override
public void run() {
synchronized ("") {
a++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
a--;
String name = Thread.currentThread().getName();
System.out.println(name + " " + "a= " + a);
}
}
}
3. Java多线程资源同步
资源同步机制是为了解决多线程访问共享数据而提出的,各种语言实现对共享资源的同步,提出了临界区的概念,解决了访问数据时的冲突问题。实现资源同步的方式有很多,比如可以通过synchronized关键字,使用lock锁,信号量等都可以实现资源同步。
synchronized :synchronized是一个悲观锁,用synchronized包起来的代码块一次只能有一个线程来访问,其他的线程只能等这个线程执行完再进入这个代码块。由此可知synchronized会降低应用程序的性能,因此在使用的时候要看是否必须。
lock :Lock支持更灵活的同步代码块结构。使用synchronized的时候,只能在同一个代码块中实现锁的获取和释放,而使用Lock可以在不同的代码块中实现。使用lock的时候要保证其在一个try/finally中,以保证出错的时候也可以释放锁 。
用一个例子来介绍这两种方式。
class Test{
public static void main(String[] args){
lockAndSync();
}
private static void lockAndSync() throws InterruptedException {
System.out.println("========lock test=========");
for (int i = 0; i < 3; i++) {
service.execute(new LockTest());
}
TimeUnit.SECONDS.sleep(10);
System.out.println("========synchronized test ========");
for (int i = 0; i < 3; i++) {
service.execute(new SynchronizedTest());
}
TimeUnit.SECONDS.sleep(10);
service.shutdown();
}
}
//lock锁
class LockTest implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
try {
//lock获得锁
lock.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() +
" " + new Date());
} catch (InterruptedException e) {
} finally {
//在finally中释放锁,防止发生死锁
lock.unlock();
}
}
}
//synchronized锁
class SynchronizedTest implements Runnable {
@Override
public void run() {
synchronized ("") {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() +
" " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果输出:
========lock test=========
pool-1-thread-3 Wed Mar 21 19:57:55 CST 2018
pool-1-thread-2 Wed Mar 21 19:57:55 CST 2018
pool-1-thread-1 Wed Mar 21 19:57:55 CST 2018
========synchronized test ========
pool-1-thread-3 Wed Mar 21 19:58:05 CST 2018
pool-1-thread-5 Wed Mar 21 19:58:06 CST 2018
pool-1-thread-4 Wed Mar 21 19:58:07 CST 2018
通过上边这个例子,查看输出发现lockTest的运行时间都是 19:57:55 说明三个线程是都运行到sleep处,然后一起等待,一起输出的,这明显是违反了我们使用锁的初衷。再看synchronizedTest,可以看到运行时间是 19:58:05 19:58:06 19:58:07 逐个运行的,这个才是我们想看到的结果,为什么会出现这样的情况呢?
这个是因为对于同步资源来说,显示锁是对象级别的锁,内部锁是类级别的锁。也就是说Lock是根随对象的,有一个对象就有一个Lock锁,所以把Lock定义为多线程的私有属性是不能起到资源同步的作用的。
下面看一个使用Lock锁,达到资源同步的例子。可以看到输出和我们的预期是一样的。
public static void main(String[] args){
final Lock lock = new ReentrantLock();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
lock.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " " + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
}
TimeUnit.SECONDS.sleep(10);
}
结果输出:
Thread-1 Wed Mar 21 20:09:39 CST 2018
Thread-0 Wed Mar 21 20:09:40 CST 2018
Thread-2 Wed Mar 21 20:09:41 CST 2018
区别:
除了上边说的Lock是对象级别的锁,Synchronized是类级别的锁,这个区别之外,他们两个还有以下几点不同。
3.1. Lock是无阻塞的,Synchronized是有阻塞的。
当A持有锁,B想要获得锁时,如果使用的Lock锁,B此时处于等待状态,如果使用Synchronized B此时处于堵塞状态。
3.2 Lock可以实现公平锁,Synchronized只能实现非公平锁。
公平锁和非公平锁的含义是。A持有锁,BCD想要获取锁,当A结束之后,系统会从BCD中拿出一个等待时间最久的来获得锁,这个就是公平锁,非公平锁就是随机拿出一个线程,让他持有锁。
3.3 Lock是代码级锁,Synchronized是JVM级的锁。
Lock是通过编码实现的,Synchronized是在运行期由JVM解释的。
4. Java多线程执行器
有了执行器,可以将县城对象交给执行器去执行,我们不用再关心线程的创建,实例化等操作。
线程池归根结底只有ThreadPoolExecutor、ScheduleThreadPoolExecutor
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:最小线程数,他是逐渐到达的。如果cps=10,现有5个线程,那么cps最多到5.
maximumPoolSize:最大线程数。
workQueue:放置线程的队列。
If there are more than corePoolSize but less than maximumPoolSize threads running, a new thread will be created only if the queue is full
JavaDoc解释为,如果当前线程数大于cps,小于mps,就放入队列中。只有当队列满了的时候,才会新建线程。(像极了资本家~)
Java还提供了Executors类,来创建ThreadPoolExecutor对象。
newSingleThreadExecutor :单线程池。一个池中只有一个线程在运行,永不超时,当有多个任务时,通过无界阻塞队列依次处理。
newCachedThreadPool :缓冲功能线程池。可以无限制创建线程(不超过Integer最大值),新增一个任务就创建一个新的线程,或者复用之前的线程,如果一个线程在60秒内没有被使用,就会被终止。
newFixedThreadPool :固定大小的线程池。在初始化时决定线程池的大 小,如果任务数多于线程数,创建阻塞队列。
5. 综述
多线程让我们充分利用系统资源,提高效率,但是是不是线程数越多,系统资源的利用率越好呢?这显然是个假命题,如果线程过多的话,系统将花费大量的时间在线程之间来回切换,会浪费更多的时间。因此在使用多线程的时候,要根据情景,限制线程池的大小。