Java——多线程总结、ThreadLocal/Volatile/synchronized/Atomic关键字

当线程被创建并启动之后,它既不是一启动就进入执行状态,也不是一直处于执行状态,在其生命周期中,要经过”新建(New)”、”就绪(Runnable)”、”运行(Running’)”、”阻塞(Blocked)”和”死亡(Dead)”五种状态。线程在创建之后,不可能一直霸占着CPU独立运行,需要在多个线程之间切换,所以大部分时间处于运行、阻塞之间切换。

Thread类中和这四种状态相关的方法:

    // 开始线程
    public void start( );
    public void run( );

    // 挂起和唤醒线程
    public void resume( );     // 不建议使用
    public void suspend( );    // 不建议使用
    public static void sleep(long millis);
    public static void sleep(long millis, int nanos);

    // 终止线程
    public void stop( );       // 不建议使用
    public void interrupt( );

    // 得到线程状态
    public boolean isAlive( );
    public boolean isInterrupted( );
    public static boolean interrupted( );

    // join方法
    public void join( ) throws InterruptedException;

线程之间的通信

1、wait和notify必须配合synchronized关键字使用

2、调用wait方法后,会释放synchronized同步的对象锁,notify方法不释放synchronized同步的对象锁

一、新建和就绪状态

  (1)、当用new关键字创建一个线程的时候,该线程就处于新建状态;
  (2)、调用start()方法之后,线程就进入了就绪状态。当不能立刻进入运行状态,要等待JVM里线程调度器的调度。
  注意:只能对处于新建状态的线程调用start()方法,否则会引发异常。
  

二、运行和阻塞状态

  (1)、如果处于就绪状态的线程获得了CPU,就开始执行run方法,处于了运行状态。当分配的时间用完后,又进入了就绪状态,等待下次分配到CPU在进入运行状态。
  (2)、遇到以下几种情况,线程会进入阻塞状态:
  1)、线程调用sleep()方法主动放弃所占用的处理器资源
  2)、线程调用了一个阻塞式IO方法,在该方法返回之时,该线程被阻塞
  3)、线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
  4)、现成在等待某个通知(notify)
  5)、线程调用了线程的suspend()方法将该线程挂起。该方法容易造成死锁,应尽量避免使用该方法。
  进入阻塞状态的程序遇到以下情况才能恢复进入就绪状态,等待分配到资源进入运行状态:
  1)、调用sleep()方法的线程经过了指定时间。
  2)、线程调用的阻塞式IO方法已经返回
  3)、线程成功的获得了试图取得的同步监视器
  4)、线程正在等待的某个通知时,其他线程发出了一个通知(signal)
  5)、处于挂起状态的线程被调用了resume()恢复方法

三、线程死亡

  (1)、run()或call()方法执行完成,线程正常结束;
  (2)、线程抛出一个未捕获的Exception或Error;
  (3)、直接调用该线程的stop()方法来结束该线程—该方法容易导致死锁,不推荐。
  注意:当主线程结束时,其他线程不收任何影响,并不会随之结束。一旦子线程启动起来后,他就拥有和主线程相同的地位,他不会受主线程的影响。
  为了测试某个线程是否已经死亡,可以调用线程对象的isAlive方法,当线程处于就绪、运行、阻塞3中状态是,该方法返回true;当线程处于新建、死亡2种状态时,该方法将返回false.
  注意:不要试图对一个已经死亡的线程调用start()方法使它重新启动,该线程不可再次作为线程执行。start方法只能对处于新建状态的线程多使用,且只能使用一次!

四、终止线程的三种方法

有三种方法可以使终止线程。

1、 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

public class ThreadFlag extends Thread
{
    public volatile boolean exit = false;

    public void run()
    {
        while (!exit);
    }
    public static void main(String[] args) throws Exception
    {
        ThreadFlag thread = new ThreadFlag();
        thread.start();
        sleep(5000); // 主线程延迟5秒
        thread.exit = true;  // 终止线程thread
        thread.join();
        System.out.println("线程退出!");
    }
}

在上面代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false。在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值。

2、使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,会释放所有的Monitor,从而发生不可预料的结果。我们很少希望一个线程立即停止,如果线程在正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态)。

3、使用interrupt方法中断线程
在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。
①静态的方法interrupted()
②非静态的方法isInterrupted(),
这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判断其他线程是否被中断。

当另一个线程通过调用 Thread.interrupt() 中断一个线程时,会出现以下两种情况:
①如果那个线程在执行一个低级可中断阻塞方法,例如 Thread.sleep()、 Thread.join() 或 Object.wait(),那么它将取消阻塞并抛出 InterruptedException。
②interrupt() 只是设置线程的中断状态。 在被中断线程中运行的代码以后可以轮询中断状态,看看它是否被请求停止正在做的事情。中断状态可以通过 Thread.isInterrupted() 来读取,并且可以通过一个名为 Thread.interrupted() 的操作读取和清除。

中断是一种协作机制。当一个线程中断另一个线程时,被中断的线程不一定要立即停止正在做的事情。相反,中断是礼貌地请求另一个线程在它愿意并且方便的时候停止它正在做的事情。有些方法,例如 Thread.sleep(),很认真地对待这样的请求,但每个方法不是一定要对中断作出响应。对于中断请求,不阻塞但是仍然要花较长时间执行的方法可以轮询中断状态,并在被中断的时候提前返回。 您可以随意忽略中断请求,但是这样做的话会影响响应。
中断的协作特性所带来的一个好处是,它为安全地构造可取消活动提供更大的灵活性。我们很少希望一个活动立即停止;如果活动在正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态。中断允许一个可取消活动来清理正在进行的工作,恢复不变量,通知其他活动它要被取消,然后才终止。

示例代码如下:

public void run() {
    while (true) {
        if (Thread.currentThread().isInterrupted()) {// ②
            System.out.println("Interrupted");
            break;
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) { // ①
            System.out.println("Interrupted when sleep");

            /**
             *  当一个阻塞方法检测到中断并抛出 InterruptedException 时,它会清除中断状态。
             *  如果捕捉到 InterruptedException 但是不能重新抛出它,那么应该保留中断发生的证据,
             *  以便调用栈中更高层的代码能知道中断,并对中断作出响应。
             *  该任务可以通过调用 interrupt() 以 “重新中断” 当前线程来完成
             */
            // 捕捉 InterruptedException 后恢复中断状态
            Thread.currentThread().interrupt(); // ②
        }

        ......

    }
}

Java 理论与实践: 处理 InterruptedException

五、join方法的使用

join方法的功能就是使异步执行的线程变成同步执行。也就是说,当调用线程实例的start方法后,这个方法会立即返回,如果在调用start方法后后需要使用一个由这个线程计算得到的值,就必须使用join方法。如果不使用join方法,就不能保证当执行到start方法后面的某条语句时,这个线程一定会执行完。而使用join方法后,直到这个线程退出,程序才会往下执行。

public class JoinThread extends Thread
{
    public static int n = 0;

    static synchronized void inc()
    {
        n++;
    }
    public void run()
    {
        for (int i = 0; i < 10; i++)
            try
            {
                inc();
                sleep(3);  // 为了使运行结果更随机,延迟3毫秒

            }
            catch (Exception e)
            {
            }                                      
    }
    public static void main(String[] args) throws Exception
    {

        Thread threads[] = new Thread[100];
        for (int i = 0; i < threads.length; i++)  // 建立100个线程
            threads[i] = new JoinThread();
        for (int i = 0; i < threads.length; i++)   // 运行刚才建立的100个线程
            threads[i].start();
        if (args.length > 0)  
            for (int i = 0; i < threads.length; i++)   // 100个线程都执行完后继续
                threads[i].join();
        System.out.println("n=" + JoinThread.n);
    }
}

如上示例:建立了100个线程,每个线程使静态变量n增加10。如果在这100个线程都执行完后输出n,这个n值应该是1000。

六、慎重使用volatile关键字

volatile关键字用于声明简单类型变量,如int、float、boolean等数据类型。如果这些简单数据类型声明为volatile,可以保证线程之间变量的可见性。对变量的操作会变成原子级别的。但这有一定的限制。例如,下面的例子中的n就不是原子级别的:

public class JoinThread extends Thread
{
    public static volatile int n = 0;

    public void run()
    {
        for (int i = 0; i < 10; i++)
            try
            {
                n = n + 1;
                sleep(3); // 为了使运行结果更随机,延迟3毫秒
            }
            catch (Exception e)
            {
            }
    }

    public static void main(String[] args) throws Exception
    {

        Thread threads[] = new Thread[100];
        for (int i = 0; i < threads.length; i++)
            // 建立100个线程
            threads[i] = new JoinThread();
        for (int i = 0; i < threads.length; i++)
            // 运行刚才建立的100个线程
            threads[i].start();
        for (int i = 0; i < threads.length; i++)
            // 100个线程都执行完后继续
            threads[i].join();
        System.out.println("n=" + JoinThread.n);
    }
}

如果对n的操作是原子级别的,最后输出的结果应该为n=1000,而在执行上面积代码时,很多时侯输出的n都小于1000,这说明n=n+1不是原子级别的操作。

n=n+1 / n++ 非原子性的原因?
n=n+1/n++只是一条语句,又如何在执行过程中将CPU交给其他的线程呢?其实这只是表面现象,n++在被Java编译器编译成中间语言(也叫做字节码)后,并不是一条语言。
java源代码:

public void run()
{
    n++;
}

被编译后的中间语言代码:

  001  public void run()
  002  {
  003      aload_0         
  004      dup             
  005      getfield
  006      iconst_1        
  007      iadd            
  008      putfield       
  009      return          
  010  }

这里涉及到一个Java内存模型的问题。

这里写图片描述

Java的内存模型分为主存储区和工作存储区。主存储区保存了Java中所有的实例。也就是说,在我们使用new来建立一个对象后,这个对象及它内部的方法、变量等都保存在这一区域,在JoinThread 类中的n就保存在这个区域。主存储区可以被所有线程共享。而工作存储区就是我们前面所讲的线程栈,在这个区域里保存了在run方法以及run方法所调用的方法中定义的变量,也就是方法变量。在线程要修改主存储区中的变量时,并不是直接修改这些变量,而是将它们先复制到当前线程的工作存储区,在修改完后,再将这个变量值覆盖主存储区的相应的变量值。

在了解了Java的内存模型后,就不难理解为什么n++也不是原子操作了。它必须经过一个拷贝、加1和覆盖的过程。因此,在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原子操作,当变量的值由自身的上一个决定时,如n=n+1、n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。

要想彻底解决这个问题,有两种方案:
1、传统方法:使用某种方法对n进行同步,也就是在同一时间只能有一个线程操作n,这也称为对n的原子操作。代码如下:

public class JoinThread extends Thread
{
    public static int n = 0;

    public static synchronized void inc()
    {
        n++;
    }
    public void run()
    {
        for (int i = 0; i < 10; i++)
            try
            {
                inc(); // n = n + 1 改成了 inc();
                sleep(3); // 为了使运行结果更随机,延迟3毫秒
            }
            catch (Exception e)
            {
            }
    }

    public static void main(String[] args) throws Exception
    {

        Thread threads[] = new Thread[100];
        for (int i = 0; i < threads.length; i++)
            // 建立100个线程
            threads[i] = new JoinThread();
        for (int i = 0; i < threads.length; i++)
            // 运行刚才建立的100个线程
            threads[i].start();
        for (int i = 0; i < threads.length; i++)
            // 100个线程都执行完后继续
            threads[i].join();
        System.out.println("n=" + JoinThread.n);
    }
}

2、使用AtomicInteger对象定义n

ThreadLocal、Volatile、synchronized、Atomic各自的应用场景

这篇文件讲的比较清楚:http://blog.csdn.net/u010687392/article/details/50549236

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值