Java多线程大总结

转载:http://50vip.com/blog.php?i=151

 

Java多线程在任何互联网公司的任何笔试任何面试几乎都会有,下面对Java的Thread多线程做一个总结,目的在于看完这篇文章,多线程的笔试面试再也不是问题。(文中标红的地方为重中之重)

主要从以下几个方面说明:

一.两种实现Java多线程的方法、注意点及区别

二.Java多线程中对于线程操作的方法

三.Java多线程中资源同步的方法


一、实现Java多线程的方法

这部分大部分人看过书应该都很清楚,有两种方法:继承Thread类、实现Runnable接口。

1.继承Thread类

大致代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.vip.thread;
/**
  * 继承Thread类实现多线程
  * @author Xewee.Zhiwei.Wang
  * @version 2013-3-25 下午3:00:21
  * @Contract wzwahl36@QQ.com or http://50vip.com/
  */
public class ThreadTest extends Thread {
     /**
      * 重写run方法
      */
     public void run() {
         for ( int i = 0 ; i < 520 ; i++) {
             System.out.println( "I Love You..." );
         }
     }
     public static void main(String[] args) {
         new ThreadTest().start();
     }
}

2.实现Runnable接口

大致代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.vip.thread;
/**
  * 多线程实现runnable接口
  * @author Xewee.Zhiwei.Wang
  * @version 2013-3-25 下午2:59:03
  * @Contract wzwahl36@QQ.com or http://50vip.com/
  */
public class RunnableTest implements Runnable {
     public void run() {
         for ( int i = 0 ; i < 520 ; i++) {
             System.out.println( "I Love You..." );
         }
     }
     public static void main(String[] args) {
         new ThreadTest().start();
     }
}

这两段代码需要注意的是:运行多线程使用的方法是start(),而不是覆写的run()方法,但是实际执行的代码块还是run()方法中的。为什么不能使用run(),主要原因是以为多线程操作会执行操作系统底层native方法,这一点你可以看start()方法的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized void start() {
     /**
      * This method is not invoked for the main method thread or "system"
      * group threads created/set up by the VM. Any new functionality added
      * to this method in the future may have to also be added to the VM.
      *
      * A zero status value corresponds to state "NEW".
      */
     if (threadStatus != 0 || this != me)
             throw new IllegalThreadStateException();
         group.add( this );
     start0();
     if (stopBeforeStart) {
         stop0(throwableFromStop);
     }
}
private native void start0();

从 代码可以看出:1.执行start()方法之后,此处调用的是start0()。并且这个这个方法用了native关键字,此关键字表示调用本地操作系统 的函数。因为多线程的实现需要本地操作系统的支持。2.start()方法多次执行会抛出 java.lang.IllegalThreadStateException异常。

3.两种实现方式的区别,如何选择?

实际上,看到Thread类的源代码之后,你就会发现其实Thread也是实现Runnable接口的:

1
2
3
4
5
6
7
class Thread implements Runnable {
     public void run() {
         if (target != null ) {
              target.run();
         }
     }
}

其实Thread中的run方法调用的是Runnable接口的run方法。不知道大家发现没有,Thread和Runnable都实现了run方法,这种操作模式其实就是代理模式,这是23种设计模式中的一种。

既然二者同宗同源,那么怎么选择使用哪种方式实现Java多线程?个人推荐使用实现Runnable接口的方法,它有以下几个优点:

1)适合多个相同的程序代码的线程去处理同一个资源;

2)可以避免java中的单继承的限制;

3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。

最后,PS一个:main方法其实也是一个线程。 在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。在java中,每次程序运行至少启动2个线程。一个是 main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实际在就是在操作系统中启动了一 个进程。


二、Java多线程操作方法

主要操作方法有:isAlive()是否存活、join()强制执行、yield()暂停、sleep()休眠、interrupt()中断等等。另外还有设置线程优先级、守护线程的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.vip.thread;
/**
  * Java线程操作
  * @author Xewee.Zhiwei.Wang
  * @version 2013-3-25 下午3:29:38
  * @Contract wzwahl36@QQ.com or http://50vip.com/
  */
public class Test implements Runnable {
     public void run() {
         for ( int i = 0 ; i < 520 ; i++) {
             System.out.println(Thread.currentThread().getName() + ": I Love You..." );
         }
     }
     public static void main(String[] args) throws InterruptedException {
         Thread t1 = new Thread( new Test(), "线程1" );
         t1.start();
         System.out.println(t1.isAlive());
         Thread.sleep( 2000 );
         System.out.println(t1.isAlive());
     }
}

上述代码中用到了isAlive()、sleep()方法,程序执行结果是先true,后false。除此之外,其他的一些方法后面我会加入更多的、更有说服力的实例代码来说明。

另外再贴一段关于Thread的wait,notify,wait,sleep简单演示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Test extends Thread {
     Object lock = null ;
     boolean notifyFlag = false ;
     public Test(Object lock, boolean notifyFlag) {
         this .lock = lock;
         this .notifyFlag = notifyFlag;
     }
     @Override
     public void run() {
         synchronized (lock) {
             System.out.println((notifyFlag == true ? "(notifyThread)" : "" ) + Thread.currentThread().getName()
                     + " hava in partA" );
             try {
                 // Thread.sleep(10000); // A
                 if (notifyFlag)
                     lock.notifyAll();  // C
                 // lock.notify();      // B
                 else
                     lock.wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println((notifyFlag == true ? "(notifyThread)" : "" ) + Thread.currentThread().getName()
                     + " hava in partB" );
         }
     }
     public static void main(String[] args) throws InterruptedException {
         Object lock = new Object();
         new Test(lock, false ).start();
         new Test(lock, false ).start();
         new Test(lock, false ).start();
         Thread.sleep( 10000 );
         new Test(lock, true ).start();
     }
}

最后,在Java多线程操作方法结束之前PS一个:主线程(main方法线程)也有可能在子线程结束之前结束。并且子线程不受影响,不会因为主线程的结束而结束。在 java程序中,只要前台有一个线程在运行,整个java程序进程不会消失,所以此时可以设置一个后台守护线程,这样即使java进程小时了,这个后台线 程依然能够继续运行。在Eclipse中执行下面的代码,你会发现Eclipse标志程序运行的红色点点变灰了之后,表示主线程已经退出消失,但是在任务 管理器中仍然可以看到javaw.exe进程,这就是守护线程的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.vip.thread;
/**
  * Java线程操作
  * @author Xewee.Zhiwei.Wang
  * @version 2013-3-25 下午3:29:38
  * @Contract wzwahl36@QQ.com or http://50vip.com/
  */
public class Test implements Runnable {
     public void run() {
         while ( true ) {
             System.out.println(Thread.currentThread().getName() + ": I Love You..." );
         }
     }
     public static void main(String[] args) throws InterruptedException {
         Thread t1 = new Thread( new Test(), "线程1" );
         t1.setDaemon( true );
         t1.start();
     }
}

 


三、Java多线程的资源同步(线程同步)

前面的一、二都是基础,几乎是用过Java多线程的孩子都知道上面的,但是简单实用过Java的确不一定知道资源或者线程的同步问题,所以这个问题也是面试官考核你对Java多线程的掌握程度。

学 过数据库系统的都知道:DBMS在进行数据库操作时必须遵守几个原则:一致性、完整性、永久性、原子性。遵守这几个原则的原因是防止多客户端操作时的数据 读脏问题,特别是对于数据一致性比较重要的时候,比如书本上最喜欢的例子就是银行存款取款问题。那么要遵守这几个原则我们在程序中该怎么做呢?那就是事务 操作,要么commit,要么rollback。

同样的,多线程操作和DBMS的事务操作类似,需要对共同访问的数据和资源进行同步(事务一致性),除此之外,线程同步还需要考虑线程造成死锁的情况,那么线程同步有哪些方式方法?所谓同步就是在同一时间,只能有一个线程在访问共享资源。

1.synchronized关键字

synchronized可以修饰一个代码块,也可以修饰一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.vip.thread;
/**
  * Java线程操作
  * @author Xewee.Zhiwei.Wang
  * @version 2013-3-25 下午3:29:38
  * @Contract wzwahl36@QQ.com or http://50vip.com/
  */
public class Test implements Runnable {
     public void run() {
         for ( int i = 0 ; i < 520 ; ++i){
             synchronized ( this ) {
                 System.out.println(Thread.currentThread().getName() + ": I Love You..." );
             }
         }
     }
     public static void main(String[] args) throws InterruptedException {
         Thread t1 = new Thread( new Test(), "线程1" );
         Thread t2 = new Thread( new Test(), "线程2" );
         t1.start();
         t2.start();
     }
}

上述代码是用synchronized修饰一个代码块的,其中,虽然有两个线程,但是这两个线程不可能同时运行被synchronized包围的代码块(本示例中并没有线程之间的共享资源)。

下面在举一个使用synchronized修饰方法的例子,这个例子很常见的就是在单例模式中(单例模式也是一个很重要的笔试面试点),直接贴代码,知道单例模式的再学习一下,不知道的好好学习一下,并google其他的博文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.vip.singleton;
/**
  * 单例模式
  * @author Xewee.Zhiwei.Wang
  * @version 2013-3-25 下午4:13:17
  * @Contract wzwahl36@QQ.com or http://50vip.com/
  */
public class Singleton {
     private static Singleton INSTANCE = null ;
     // Private constructor suppresses
     private Singleton() {}
     // default public constructor
     public synchronized static Singleton getInstance() {
         if (INSTANCE == null ) {
             INSTANCE = new Singleton();
         }
         return INSTANCE;
     }
}

写在synchronized的最后,还是PS一下:synchronized是一个大考点,需要说明一下的情况是:当一个类A的多个方法(b、c)都用synchronized修饰时,再new 2了线程,这两个线程持有同一个A的对象时,线程1调用b方法的同时,线程2可以调用c方法吗?答案是不行的。因此,在一个项目中如果过多的使用synchronized关键字,将会导致过多资源的同步,首先造成的就是程序执行效率低,另外一个问题就是容易造成死锁:因此,线程资源同步的粒度越小越好...

有关于synchronized的问题,我会另外开博文详细说明清楚,这里就点到为止。

2.java.util.concurrent.locks包下的接口Lock

Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作,它能以更优雅的方式处理线程同步问题。示代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class LockTest {
     public static void main(String[] args) {
         final Outputter1 output = new Outputter1();
         new Thread() {
             public void run() {
                 output.output( "zhangsan" );
             };
         }.start();     
         new Thread() {
             public void run() {
                 output.output( "lisi" );
             };
         }.start();
     }
}
class Outputter1 {
     private Lock lock = new ReentrantLock(); // 锁对象
     public void output(String name) {
         // TODO 线程输出方法
         lock.lock(); // 得到锁
         try {
             for ( int i = 0 ; i < name.length(); i++) {
                 System.out.print(name.charAt(i));
             }
         } finally {
             lock.unlock(); // 释放锁
         }
     }
}

这 样就实现了和sychronized一样的同步效果,需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用 Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内。

如果说这 就是Lock,那么它不能成为同步问题更完美的处理方式,下面要介绍的是读写锁(ReadWriteLock),我们会有一种需求,在对数据进行读写的时 候,为了保证数据的一致性和完整性,需要读和写是互斥的,写和写是互斥的,但是读和读是不需要互斥的,这样读和读不互斥性能更高些,这种精确的加锁方式是 synchronized无法实现的。来看一下不考虑互斥情况的代码原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ReadWriteLockTest {
     public static void main(String[] args) {
         final Data data = new Data();
         for ( int i = 0 ; i < 3 ; i++) {
             new Thread( new Runnable() {
                 public void run() {
                     for ( int j = 0 ; j < 5 ; j++) {
                         data.set( new Random().nextInt( 30 ));
                     }
                 }
             }).start();
         }      
         for ( int i = 0 ; i < 3 ; i++) {
             new Thread( new Runnable() {
                 public void run() {
                     for ( int j = 0 ; j < 5 ; j++) {
                         data.get();
                     }
                 }
             }).start();
         }
     }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Data {   
     private int data; // 共享数据
     private ReadWriteLock rwl = new ReentrantReadWriteLock();  
     public void set( int data) {
         rwl.writeLock().lock(); // 取到写锁
         try {
             System.out.println(Thread.currentThread().getName() + "准备写入数据" );
             try {
                 Thread.sleep( 20 );
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             this .data = data;
             System.out.println(Thread.currentThread().getName() + "写入" + this .data);
         } finally {
             rwl.writeLock().unlock(); // 释放写锁
         }
     }  
     public void get() {
         rwl.readLock().lock(); // 取到读锁
         try {
             System.out.println(Thread.currentThread().getName() + "准备读取数据" );
             try {
                 Thread.sleep( 20 );
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName() + "读取" + this .data);
         } finally {
             rwl.readLock().unlock(); // 释放读锁
         }
     }
}

这个代码就是说明如何使用手动加锁的方式进行线程资源同步,并获得更高的性能。

3.ThreadLocal

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

在 同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写, 什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。而ThreadLocal则从另一个角度来解决多线程的并发访 问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就 没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

由 于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换 空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变 量,因此可以同时访问而互不影响。

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

void set(Object value)

设置当前线程的线程局部变量的值。

public Object get()

该方法返回当前线程所对应的线程局部变量。

public void remove()

将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

protected Object initialValue()

返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

值 得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进 行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

有关于ThreadLocal的内容非常之多,很多数据库连接池就是基于ThreadLocal的,例如OSChina的数据库链接池就是使用ThreadLocal自己封装的。关于ThreadLocal的内容大家可以自己再搜索学习一下。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值