多线程学习

来源:互联网 发布:照片看图软件 编辑:程序博客网 时间:2024/06/03 22:45

实现多线程的方式

1.implements Runnable

public class MyRunnable implements Runnable {    public void run() {        System.out.println("实现了Runnable接口,当线程启动时就会调用run()");    }    public static void main(String[] args){        Runnable runnable = new MyRunnable();        Thread thread = new Thread(runnable);        thread.start();    }}

2.extends Thread

/** * 不推荐此种方式!!! */public class MyThread extends Thread{    @Override    public void run() {        super.run();    }}

注意:不要调用Thread类或者Runnable对象的run方法。直接调用只会执行同一个线程中的任务,而不会启动新线程。应该调用Thread.start方法,该方法将创建一个执行run方法的新线程。

中断线程

弃用的方法:

thread.stop();thread.suspend();thread.resume();

初始的java版本定义了一个stop方法来终止一个线程,suspend方法来阻塞一个线程直至另一个线程调用resume。
stop方法天生就不安全,suspend方法经常导致死锁,因此都被弃用了。
每个线程都具有一个boolean标志的中断状态,当对一个线程调用interrupt方法时,线程的中断状态将被重置。

thread.interrupt();//请求终止线程

每个线程都应该不时的检查这个标志,以判断线程是否被中断。

 while (!Thread.currentThread().isInterrupted()){            for (int i=0;i<1000;i++){                System.out.println("运行中:"+i);            }        }

如果线程被阻塞,就无法检测中断状态。如果在循环中调用sleep方法,不会检测中断状态。在中断状态被置位时调用sleep方法,它不会休眠,相反它将清除这一状态并抛出InterruptedException。
如下代码,如果未被请求终止之前线程运行了则会循环完成整个方法再停止。

public class MyRunnable implements Runnable {    public void run() {        System.out.println("实现了Runnable接口,当线程启动时就会调用run()");        try {            while (!Thread.currentThread().isInterrupted()){                for (int i=0;i<1000;i++){                    System.out.println("运行中:"+i);                }                Thread.sleep(1000);            }        }catch (InterruptedException e){            System.out.println("线程被中断了。。。");        }    }    public static void main(String[] args) throws Exception{        Runnable runnable = new MyRunnable();        Thread thread = new Thread(runnable);        thread.start();        System.out.println("我要请求终止线程喽~");        thread.interrupt();//请求终止线程    }}

可能的运行结果:

实现了Runnable接口,当线程启动时就会调用run()
运行中:0
运行中:1
。。。
运行中:126
我要请求终止线程喽~
运行中:127
。。。。
运行中:999
线程被中断了。。。

推荐的处理InterruptedException的方法:
尽量不要像如上代码那样处理,不要将InterruptedException抑制在很低的层次上。比较合理的操作是:
1.在catch子句中设置中断状态,调用者可以对其进行检测

try {        }catch (InterruptedException e){            Thread.currentThread().interrupt();        }

2.直接抛出该异常,调用者捕获该异常进行处理。

throws InterruptedException
  • interrupt() 向线程发送中断请求,中断状态设为true
  • interrupted() 静态方法,检测线程是否被中断,并清除中断状态
  • isInterrupted() 实例方法,检测线程是否被中断
  • currentThread() 返回代表当前执行线程的Thread对象

线程状态

  • New 新创建
  • Runnable 可运行
  • Blocked 被阻塞
  • Waiting 等待
  • Timed waiting 计时等待
  • Terminated 被终止
    新创建线程
    用new操作符创建一个线程时,如new Thread(runnable),该线程还没有开始运行。它的状态就是New
    可运行线程
    一旦调用start方法,线程处于runnable状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
    被阻塞线程和等待线程
    当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。
  • 当一个线程试图获取一个内部的对象锁(非java.util.concurrent库中的锁),而该锁被其他线程所持有,则该线程进入阻塞状态。
  • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。
  • 有几个方法有一个超时参数(如Thread.sleep(),Object.wait等)。调用它们导致线程进入计时等待状态。
    被终止的线程
  • 因为run方法正常退出而自然死亡
  • 因为一个没有捕获的异常终止了run方法而意外死亡
    相关方法:
  • State getState() :得到线程的状态
public enum State {        NEW,        RUNNABLE,        BLOCKED,        WAITING,        TIMED_WAITING,        TERMINATED;    }
  • join
/* 等这个线程死亡:Waits for this thread to die.*/public final void join() throws InterruptedException {        join(0);    }
  • join(long millis)等待指定的毫秒数

线程属性

线程的优先级
在java中,每一个线程有一个优先级。默认情况下,一个线程继承它父类的优先级。

 thread.setPriority(1);//设置优先级        Thread.MAX_PRIORITY;//最高优先级 10        Thread.MIN_PRIORITY;//最低优先级 1        Thread.NORM_PRIORITY;//默认优先级 5

每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但线程的优先级高度依赖与系统
守护线程

thread.setDaemon(true);//该线程转换为守护线程,该方法必须在线程启动之前调用

未捕获异常处理器
线程的run方法不能抛出任何被检测的异常。但是不被检测的异常会导致线程终止,在这种情况下,线程就死亡了。
但是,不需要任何catch子句来处理可以被传播的异常。相反,就在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。
该处理器必须属于一个实现TThread.UncaughtExceptionHandler接口的类,该接口只有一个方法:

/**         * Method invoked when the given thread terminates due to the given uncaught exception.         * Any exception thrown by this method will be ignored by the Java Virtual Machine.         */        void uncaughtException(Thread t, Throwable e);

可以用setUncaughtExceptionHandler()为线程安装一个处理器,也可以用Thread.setDefaultUncaughtExceptionHandler()为所有线程安装一个默认的处理器。
如果不安装默认的处理器,默认为空。但如果不为独立的线程安装处理器,此时处理器就是该线程的ThreadGroup对象。
ThreadGroup类 implements Thread.UncaughtExceptionHandler 。其uncaughtException方法:

 public void uncaughtException(Thread t, Throwable e) {        if (parent != null) {            parent.uncaughtException(t, e);        } else {            Thread.UncaughtExceptionHandler ueh =                Thread.getDefaultUncaughtExceptionHandler();            if (ueh != null) {                ueh.uncaughtException(t, e);            } else if (!(e instanceof ThreadDeath)) {                System.err.print("Exception in thread \""                                 + t.getName() + "\" ");                e.printStackTrace(System.err);            }        }    }
  • 如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用
  • 否则,如果Thread.UncaughtExceptionHandler返回一个非空的处理器则调用该处理器
  • 否则,如果是ThreadDeath的一个实例,什么也不做
  • 否则,线程的名字以及Throwable的栈踪迹被输出到System.err上。

同步

有两种机制防止代码块受并发访问的干扰

  • synchronized 关键字
  • ReentrantLock
    用ReentrantLock保护代码块的基本结构:
myLock().lock();try{}finally{    myLock.unlock();}

这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其它线程都无法通过lock语句访问。当其它线程调用lock时被阻塞,直到第一个线程释放锁对象。
每一个对象都有自己的ReentrantLock对象,如果两个线程试图访问同一个对象那么锁以串行的方式提供服务。但是两个线程访问不同的对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞,线程之间不会相互影响。
锁是可重入的,因为线程可以重复的获得以及持有的锁。锁保持一个持有技术(hold count)来跟踪对lock方法的嵌套调用。线程在每一次调用lock方法都需要调用unlock方法来释放锁。
条件对象
通常,线程进入临界区,却发现在某一条件满足之后才能执行。
一个锁对象可以有一个或多个相关的条件对象。

 private Lock bankLock = new ReentrantLock(); private Condition sufficientFunds = bankLock.newCondition();

一旦发现条件不满足,它调用:

sufficientFunds.await();

当前线程被阻塞了,并放弃了锁。它进入了该条件的等待集。当锁可用时,它需要等待另一个线程调用同一条件上的signalAll方法为止。

sufficientFunds.signalAll();

signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。
至关重要的是最终需要某个其它线程调用signalAll方法。当一个线程调用await时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁现象。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用await方法,那么它也被阻塞。没有任何线程可以解除其他线程的阻塞,那么程序就挂起了。
有关锁和条件的关键之处:

  • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
  • 锁可以管理试图进入被保护代码段的线程
  • 锁可以拥有一个或多个相关的条件对象
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程
    synchronized关键字
    从1.0版本开始,java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
public synchronized void method(){}

等价于

public void method(){    this.intrinsicLock.lock();    try{}    finally{this.intrinsicLock.unlock();}}

内部对象锁只有一个相关条件,wait添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。调用wait/notifyAll等价于:

intrinsicCondition.await();intrinsicCondition.signalAll();

内部锁和条件存在的一些局限:
1. 不能中断一个正在试图获取锁的线程
2. 试图获得锁时不能设定超时
3. 每个锁仅有单一的条件,可能是不够的。
关于Lock和Condition以及同步方法的一些建议:
1. 最好既不使用Lock/Condition也不使用synchronized关键字。许多情况下可以用java.util.concurrent包中的一种机制,它会为你处理所有的加锁。
2. 如果synchronized关键字适合你的程序,那么请尽量使用它,这样可以减少编写代码的数量,减少出错的几率。
3. 如果特别需要Lock/Condition结构提供的独有特性,才使用Lock/Condition。
同步阻塞

synchronized(obj){}

监视器概念
1. 监视器是只包含私有域的类
2. 每个监视器类的对象有一个相关的锁
3. 使用该锁对所有的方法进行加锁。即,如果客户端调用obj.method(),那么obj对象的锁在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时,没有其它线程能访问该域。
4. 该锁可以有任意多个相关条件
java中的每一个对象有一个内部的锁和内部的条件。如果一个方法用synchronized关键字声明,那么它表现的就像是一个监视器方法。然而,下述的3个方面java对象不同于监视器,从而使线程安全性下降
1. 域不要求必须是private
2. 方法不要求必须是synchronized
3. 内部锁对客户是可用的

volatile
volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

private volatile boolean done = false;

注意:volatile变量不能提供原子性
final
当一个域声明为final时,其他线程会在构造函数完成构造后才能看待该变量。

    final Map<String,Double> map = new HashMap<String, Double>();

如果不使用final,就不能保证其他线程看到的是该变量更新后的值,它们可能都只是看到null。
当然,对这个映射表的操作并不是线程安全的。如果多个线程读写这个映射表,仍然需要进行同步。
原子性
假设对共享变量除了赋值之外并不完成其它操作,那么可以声明为volatile。
java.util.concurrent.atomic包中很多类使用了很高效的机器级指令来保证其操作的原子性。
阻塞队列
对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插入元素,消费者线程则取出它们。使用队列,可以安全的从一个线程向另一个线程传递数据。
当试图向队列中添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列(blocking queue)导致线程阻塞。
在协调多个线程之间的合作时,阻塞队列是一个有用的工具。工作者线程可以周期性地将中间结果存储在阻塞队列中。其他工作者线程移出中间结果并进一步加以修改。队列会自动平衡负载。
这里写图片描述

如果将队列当作线程管理工具来使用,将要用到put和take方法。
当试图向满的队列添加或从空的队列中移除元素时,add、remove和element操作抛出异常。当然,在一个多线程程序中,队列会在任何时候空或满,因此,一定要使用offer、poll和peek方法作为代替,这些方法如果不能完成任务,只是给出一个错误提示而不会抛出异常。
注意:poll和peek方法返回空来表示失败,因此,向这些队列中插入null是非法的。

原创粉丝点击