Java多线程概述

 多线程,又称之为并发(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. 综述

 多线程让我们充分利用系统资源,提高效率,但是是不是线程数越多,系统资源的利用率越好呢?这显然是个假命题,如果线程过多的话,系统将花费大量的时间在线程之间来回切换,会浪费更多的时间。因此在使用多线程的时候,要根据情景,限制线程池的大小。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值