Java多线程基础

来源:互联网 发布:java素数的判断 编辑:程序博客网 时间:2024/06/16 17:23

Java多线程基础

@(JAVA)[java]

  • Java多线程基础
  • 一概述
    • 一基础内容
      • 1线程的基本概念
      • 2JAVA线程基础
      • 3创建新线程的2种方法
    • 二线程状态及其变迁
      • 1Java线程的六种状态
      • 2线程状态的变迁
  • 二常用API
    • 一1创建启动线程
    • 二终止线程的方法
    • 三线程优先级
    • 四waitnofitynotifyAll
    • 五一些deprecated的API
    • 六Daemon线程
    • 七sleep
    • 八volatile
    • 九synchronized
    • 十ThreadLocal
        • 一个例子
        • 如何创建ThreadLocal变量
        • 如何访问ThreadLocal变量
        • 如何初始化ThreadLocal变量的值
        • 知其然
        • 知其所以然
    • 十一join
    • 十二Lock
  • 三CallableExecutorFuture
    • 一基本概念
    • 二定时任务
      • 1Timer VS ScheduledThreadPoolExecutor
      • 2ScheduledThreadPoolExecutor示例

一、概述

(一)基础内容

1、线程的基本概念

(1)一个线程是一个程序内部的顺序控制流。

(2)线程和进程
–每个进程都有独立的代码和数据空间(进程上下文),进程切换的开销大。

–线程:轻量的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。

–多进程:在操作系统中,能同时运行多个任务(程序)。

–多线程:在同一应用程序中,有多个顺序流同时执行。

(3)线程间的通信一般有2种方法:共享内存和消息传递。在共享内存模型中,多个线程通过读写公共内存中的变量,实现隐式通信。而在消息传递模型中,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。

JAVA主要使用共享内存进行线程通信,公共变量会保存在JVM中的堆内存,多个线程可以同时访问这些内存内容。重复一下,堆中主要保存的是类实例变量、静态变量、数组等,而栈保存了局部变量、参数等。
但同时JAVA也有一些消息传递的情形,如notify等。

2、JAVA线程基础

JAVA的线程是通过java.lang.Thread类来实现的。

VM启动时会有一个由main()方法所定义的线程。

可以通过创建Thread的实例来创建新的线程。

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。

通过调用Thread类的start()方法来启动一个线程。

3、创建新线程的2种方法

(1)继承Thread类

(2)实现Runnable接口

建议使用第二种方式,即实现Runnable接口。

(二)线程状态及其变迁

1、Java线程的六种状态

状态名称 说明 NEW 初始状态,线程被构建,但还没有调用start()方法。 RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态统一称为RANNABLE BLOCK 阻塞状态,表示线程阻塞于锁 WAITING 等待状态,表示当前线程需要等待其它线程做出一些待定动作(通知或中断) TIME_WAITING 超时等待状态,类似于WAITING,但可以在指定时间后自行返回 TERMINATED 终止状态,表示当前线程已经执行完毕

2、线程状态的变迁

image

二、常用API

(一)1、创建&启动线程

进程需要实现Runnable接口或者继承Thread类,一般用前者。然后创建一个线程对象,并调用其start()方法。

public class CreateThread {    public static void main(String[] args) {        //2、使用线程类创建一个线程对象        PrintDate pd = new PrintDate();        Thread t = new Thread(pd);        //3、启动线程        t.start();    }}//1、创建线程类class PrintDate implements Runnable {    public void run() {        for (int i = 0; i < 100; i++) {            System.out.println(new Date());        }    }}

(二)终止线程的方法

当一个线程中的所有代码被执行完成后,这个线程就会终止。但很多情况下,线程的run()方法会使用循环一直执行,直到显式通知它线程终止。通知线程终止的方法有2种:(1)使用一个boolean值来通知线程(2)使用线程的中断标准位。

public class TerminateThread {    public static void main(String[] args) throws InterruptedException {        //1、使用interrupt()方法终止线程        Runner runnerOne = new Runner();        Thread threadOne = new Thread(runnerOne);        threadOne.start();        Thread.sleep(5000);        threadOne.interrupt();        //2、使用boolean对象终止线程        Runner runnerTwo = new Runner();        Thread threadTwo = new Thread(runnerTwo);        threadTwo.start();        Thread.sleep(5000);        runnerTwo.cancle();    }}class Runner implements Runnable {    private static boolean on = true;    @Override    public void run() {        while (on && !Thread.currentThread().isInterrupted()) {            System.out.println("Current time is " + Calendar.getInstance().getTime());        }    }    public void cancle() {        on = false;    }}

(三)线程优先级

可以通过设置线程的优先级以使得某些线程可以优先执行,但不要将程序的正确性依赖于线程的优先级,因为不同系统对线程优先级有不同的处理方式,有些系统甚至会忽略线程优先级。

public class ThreadPriority {    public static void main(String[] args) {        Runnable r = new MyThread();        Thread t = new Thread(r,"t1");        System.out.println(t.getPriority());//默认优先级为5        System.out.println(Thread.MAX_PRIORITY);//最大优先级为10        System.out.println(Thread.MIN_PRIORITY);//最小优先级为1        t.setPriority(Thread.MIN_PRIORITY);        t.start();        Thread t2 = new Thread(r,"t2");        t2.start();        Thread t3 = new Thread(r,"t3");        t3.setPriority(Thread.MAX_PRIORITY);        t3.start();    }}class MyThread implements Runnable{    public void run() {        for(int i = 0; i < 100; i++){            System.out.println(Thread.currentThread().getName());        }    }}

(四)wait()/nofity()/notifyAll()

这3个方法均定义在Object中,即任何java对象均有这3个方法。
一个经典应用是生产者消费者模式。当消费者消费消息时,若发现没有消息,则调用wait()方法等待生产,而生产者生产完消息后则调用notify()/notifyAll()来通知消费者。

(五)一些deprecated的API

stop()用interrupt()代替
susppend()用wait()代替
resume()用nofity()/nofityAll()代替

这些旧的API在调用后,线程不会释放已经占用的资源(如锁),容易引起死锁。

(六)Daemon线程

Daemon线程被用到完成支持性的线程,但在在JVM退出时Daemon线程中的finally模块并不一定会执行,因此不能依靠Daemon线程的finally中的内容来关闭或者清理资源。
设置daemon线程的方法如下:

thread.setDaemon(true);

当一个JVM中不存在非Daemon线程时,JVM将退出。

(七)sleep()

Thead.sleep(Long)会sleep一段时间,但如果在此期间线程的interrupt()方法被调用,则会抛出InterruptedException,同时会将interrupte标志位复原。

(八)volatile

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。
关键字volatile可以用来修饰字段(成员变量),就是告诉程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
但是过多的使用volatile是不必要的,因为它会降低程序执行的效率。

详细可参考:http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

* Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;*与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。本文介绍了几种有效使用 volatile 变量的模式,并强调了几种不适合使用 volatile 变量的情形。

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。

(九)synchronized

synchronized可以修饰方法或者以同步块的形式来使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步快中,它保证了线程对变量访问的可见性和排它性。
任何一个对象都有自己的监视器(monitor),当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到这个对象的monitor才能进入同步块或者同步方法,而没有获取到monitor的线程将会被阻塞在同步块或者同步方法的入口处,进入BLOCKED的状态。

(十)ThreadLocal

ThreadLocal,即线程变量,是一个以ThreadLocal对象对键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。比如下面例子中介绍的为每个线程指定一个数据库的Connection。
可以通过set(T)来设置一个值,在当前线程下再通过get()来获取到原先设置的值。

Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。

一个例子

public static void main(String[] args) throws InterruptedException {    Runnable shareRunnableInstance = new MyRunnable();    Thread t1 = new Thread(shareRunnableInstance);    Thread t2 = new Thread(shareRunnableInstance);    t1.start();    Thread.sleep(5000);    t2.start();}}class MyRunnable implements Runnable {    private ThreadLocal<Long> threadCreateTime = new ThreadLocal<Long>() {        @Override        protected Long initialValue() {            return 0L;        }    };    @Override    public void run() {        threadCreateTime.set(System.currentTimeMillis());        System.out.println(threadCreateTime.get());    }}

上面的例子创建了一个ThreadLocal变量用于记录线程的创建时间,它只需要创建一个Runnable对象即可使用同一个对象创建多个线程,这是ThreadLocal的一个重要应用。事实上,如果创建多个Runnable对象,那不使用ThreadLocal也能达到相同的目的,因为每个Runnable对象有自己独立的变量:

public class ThreadLocalDemo {    public static void main(String[] args) throws InterruptedException {        Runnable r1 = new MyRunnable2();        Runnable r2 = new MyRunnable2();        Thread t3 = new Thread(r1);        Thread t4 = new Thread(r2);        t3.start();        Thread.sleep(5000);        t4.start();    }}class MyRunnable2 implements Runnable {    private Long threadCreateTime = 0L;    @Override    public void run() {        threadCreateTime = System.currentTimeMillis();        System.out.println(threadCreateTime);    }}

如何创建ThreadLocal变量

以下代码展示了如何创建一个ThreadLocal变量:

private ThreadLocal myThreadLocal = new ThreadLocal();

我们可以看到,通过这段代码实例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值。

如何访问ThreadLocal变量

一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值:

myThreadLocal.set("A thread local value”);

可以通过下面方法读取保存在ThreadLocal变量中的值:

String threadLocalValue = (String) myThreadLocal.get();

get()方法返回一个Object对象,set()对象需要传入一个Object类型的参数。
为ThreadLocal指定泛型类型

我们可以创建一个指定泛型类型的ThreadLocal对象,这样我们就不需要每次对使用get()方法返回的值作强制类型转换了。下面展示了指定泛型类型的ThreadLocal例子:

private ThreadLocal myThreadLocal = new ThreadLocal<String>();

现在我们只能往ThreadLocal对象中存入String类型的值了。

并且我们从ThreadLocal中获取值的时候也不需要强制类型转换了。

如何初始化ThreadLocal变量的值

由于在ThreadLocal对象中设置的值只能被设置这个值的线程访问到,线程无法在ThreadLocal对象上使用set()方法保存一个初始值,并且这个初始值能被所有线程访问到。

但是我们可以通过创建一个ThreadLocal的子类并且重写initialValue()方法,来为一个ThreadLocal对象指定一个初始值。就像下面代码展示的那样:

private ThreadLocal myThreadLocal = new ThreadLocal<String>() {    @Override    protected String initialValue() {        return "This is the initial value";    }    };

上面的例子创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行run()方法,并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并且调用的set()方法被同步了,则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是一个ThreadLocal对象,因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。
关于InheritableThreadLocal

InheritableThreadLocal类是ThreadLocal类的子类。ThreadLocal中每个线程拥有它自己的值,与ThreadLocal不同的是,InheritableThreadLocal允许一个线程以及该线程创建的所有子线程都可以访问它保存的值。

知其然

synchronized这类线程同步的机制可以解决多线程并发问题,在这种解决方案下,多个线程访问到的,都是同一份变量的内容。为了防止在多线程访问的过程中,可能会出现的并发错误。不得不对多个线程的访问进行同步,这样也就意味着,多个线程必须先后对变量的值进行访问或者修改,这是一种以延长访问时间来换取线程安全性的策略。

而ThreadLocal类为每一个线程都维护了自己独有的变量拷贝。每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了,那就没有任何必要对这些线程进行同步,它们也能最大限度的由CPU调度,并发执行。并且由于每个线程在访问该变量时,读取和修改的,都是自己独有的那一份变量拷贝,变量被彻底封闭在每个访问的线程中,并发错误出现的可能也完全消除了。对比前一种方案,这是一种以空间来换取线程安全性的策略。

来看一个运用ThreadLocal来实现数据库连接Connection对象线程隔离的例子。

import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;public class ConnectionManager {    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {        @Override        protected Connection initialValue() {            Connection conn = null;            try {                conn = DriverManager.getConnection(                        "jdbc:mysql://localhost:3306/test", "username",                        "password");            } catch (SQLException e) {                e.printStackTrace();            }            return conn;        }    };    public static Connection getConnection() {        return connectionHolder.get();    }    public static void setConnection(Connection conn) {        connectionHolder.set(conn);    }}

通过调用ConnectionManager.getConnection()方法,每个线程获取到的,都是和当前线程绑定的那个Connection对象,第一次获取时,是通过initialValue()方法的返回值来设置值的。通过ConnectionManager.setConnection(Connection conn)方法设置的Connection对象,也只会和当前线程绑定。这样就实现了Connection对象在多个线程中的完全隔离。在Spring容器中管理多线程环境下的Connection对象时,采用的思路和以上代码非常相似。

知其所以然

那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:

/** * Sets the current thread's copy of this thread-local variable * to the specified value.  Most subclasses will have no need to  * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of *        this thread-local. */public void set(T value) {    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null)        map.set(this, value);    else        createMap(t, value);}

没有什么魔法,在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。

线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。

为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:

ThreadLocalMap getMap(Thread t) {        return t.threadLocals;    }void createMap(Thread t, T firstValue) {        t.threadLocals = new ThreadLocalMap(this, firstValue);    }

代码已经说的非常直白,就是获取和设置Thread内的一个叫threadLocals的变量,而这个变量的类型就是ThreadLocalMap,这样进一步验证了上文中的观点:每个线程都有自己独立的ThreadLocalMap对象。打开java.lang.Thread类的源代码,我们能得到更直观的证明:

/* ThreadLocal values pertaining to this thread. This map is maintained     * by the ThreadLocal class. */    ThreadLocal.ThreadLocalMap threadLocals = null;

那么接下来再看一下ThreadLocal类中的get()方法,代码是这么说的:

/**     * Returns the value in the current thread's copy of this     * thread-local variable.  If the variable has no value for the     * current thread, it is first initialized to the value returned     * by an invocation of the {@link #initialValue} method.     *     * @return the current thread's value of this thread-local     */    public T get() {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null) {            ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null)                return (T)e.value;        }        return setInitialValue();    }    /**     * Variant of set() to establish initialValue. Used instead     * of set() in case user has overridden the set() method.     *     * @return the initial value     */    private T setInitialValue() {        T value = initialValue();        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            createMap(t, value);        return value;    }

这两个方法的代码告诉我们,在获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。

进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。

设置到这些线程中的隔离变量,会不会导致内存泄漏呢?ThreadLocalMap对象保存在Thread对象中,当某个线程终止后,存储在其中的线程隔离的变量,也将作为Thread实例的垃圾被回收掉,所以完全不用担心内存泄漏的问题。在多个线程中隔离的变量,光荣的生,合理的死,真是圆满,不是么?

最后再提一句,ThreadLocal变量的这种隔离策略,也不是任何情况下都能使用的。如果多个线程并发访问的对象实例只允许,也只能创建那么一个,那就没有别的办法了,老老实实的使用同步机制来访问吧。

(十一)join()

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才会thread.join()方法返回并继续向下执行。

(十二)Lock

Lock在JDK5以后引入,它与synchronized类似,提供了锁的机制,任何时候只有一个线程能进入lock()与unlock()之间的代码。

三、Callable/Executor/Future

(一)基本概念

Runnable封装一个异步运行的任务,可以把它想象成为一个没有参数和返回值的异步方法。
Callabe与Runnable类似,但它有返回值。

public interface Callable<V> {    /**     * Computes a result, or throws an exception if unable to do so.     *     * @return computed result     * @throws Exception if unable to compute a result     */    V call() throws Exception;}

一般而言,Callable与Future结合来获取线程的计算结果。此外,还有FutureTask可以将Callable转换为Future和Runnalbe,因为它自身实现了后面2个接口。

基本使用方法如下:

1、创建一个Callable对象

MatchCounter <Integer> task = new MatchCounter(file,keyword);

其中MatchCounter实现了Callable接口。

2、将Callable对象转换为Runnalb对象

FutureTask<Integer> task = new FutureTask<>(counter);

3、将线程的返回结果保存在一个Future组成的List中

List<Future<Integer>> results = new ArrayList<>();results.add(task);

4、启动线程

Thread t = new Thread(task);t.start();

5、处理线程的计算结果

for(Future<Integer> result: results){    System.out.println(task.get());}

上面假设是起了多个线程,并且有多个返回值的情况。如果只起一个线程,则步骤为:

1、创建一个Callable对象

MatchCounter <Integer> task = new MatchCounter(file,keyword);

其中MatchCounter实现了Callable接口。

2、将Callable对象转换为Runnalb对象

FutureTask<Integer> task = new FutureTask<>(counter);

3、启动线程

Thread t = new Thread(task);t.start();

4、处理线程的计算结果

System.out.println(task.get());

因为task也是一个Future对象。

其中get()方法是阻塞的,它会等到线程执行完成后再返回。或者可以加上一个参数指定超时返回。

**可以看出,Callable的处理方法和Runnable基本一致,只是通过一个FutureTask对象来启动线程以及获取结果。

(二)定时任务

1、Timer VS ScheduledThreadPoolExecutor

http://stackoverflow.com/questions/409932/java-timer-vs-executorservice

According to Java Concurrency in Practice:

(1)Timer can be sensitive to changes in the system clock, ScheduledThreadPoolExecutor isn’t.
(2) Timer has only one execution thread, so long-running task can delay other tasks. ScheduledThreadPoolExecutor can be configured with any number of threads. Furthermore, you have full control over created threads, if you want (by providing ThreadFactory).
(3)Runtime exceptions thrown in TimerTask kill that one thread, thus making Timer dead :-( … i.e. scheduled tasks will not run anymore. ScheduledThreadExecutor not only catches runtime exceptions, but it lets you handle them if you want (by overriding afterExecute method from ThreadPoolExecutor). Task which threw exception will be canceled, but other tasks will continue to run.

2、ScheduledThreadPoolExecutor示例

以下程序每秒打印一次当前时间。ScheduledThreadPoolExecutor的使用非常简单。

public class ScheduledThreadPoolExecutorDemo {    public static void main(String[] args) throws InterruptedException {        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(10);        PrintTime task = new PrintTime();        //2秒后开始执行task,只执行一次。        //executor.schedule(task, 2, TimeUnit.SECONDS);        //1秒后开始执行task,每秒执行一次        executor.scheduleAtFixedRate(task, 1, 1, TimeUnit.SECONDS);        //1秒后开始执行task,在一次任务执行完成后1秒再重复执行task,如此循环。        //executor.scheduleWithFixedDelay(task, 1, 1, TimeUnit.SECONDS);    }} class PrintTime implements Runnable{    @Override    public void run() {        System.out.println(System.currentTimeMillis());    }}
原创粉丝点击