Java SE学习笔记-多线程

来源:互联网 发布:债券 知乎 编辑:程序博客网 时间:2024/05/29 03:19


1、主要内容

2、详细内容

2.1、概念

2.1.1、与进程的比较

(1)在操作系统中,所有运行中的任务通常对应一个进程,每个运行中程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程;

(2)线程包括三个特性:独立性;动态性以及并发性。

(3)线程也被称作轻量级进程(Lightweight Process),线程是进程的执行单元;线程是独立运行的并且线程的执行是抢占式的;

(4)一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行;

(5)一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但是至少包含一个线程。

 

2.1.2、多线程优缺点

(1)进程之间不可以共享内存,但线程之间易于共享内存;

(2)使用多线程来实现多任务并发比多进程的效率高;

(3)Java语言内置了多线程功能支持;

(4)线程太多会导致效率的降低,因为线程的执行依靠的是CPU的来回切换。

(5)多线程是指一个进程中有多个线程。

 

2.2、线程的创建和启动

2.2.1、继承Thread

线程是程序中的执行线程,Java虚拟机允许应用程序并发的运行多个执行线程;Thread类的语法格式如下所示:

public class Thread extends Object implements Runnable

通过继承Thread类创建并启动多线程的步骤有三步:

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程需要完成的任务,run方法格式如下所示:

public void run()

(2)创建Thread子类的实例,即创建了线程对象;

(3)调用线程对象的start()方法来启动该线程;start方法格式如下所示:

public void start()

示例代码如下所示:

public class FirstThread extends Thread{private int i;// 重写run方法,run方法的方法体就是线程执行体public void run(){for (; i < 100; i++){//当线程类继承Thread类时,直接使用this即可获取当前线程,Thread对象的getName()返回当前线程的对象;//因此可以调用getName()直接返回当前线程的名字System.out.println(getName() + "" + i);}}public static void main(String[] args){for (int i = 0; i < 100; i++){System.out.println(Thread.currentThread().getName() + "" + i);if (i == 20){// 创建并启动第一个线程new FirstThread().start();// 创建并启动第二个线程new FirstThread().start();}}}}

以上程序中使用了Thread类中重要的两个方法:

public static Thread currentThread()

返回对当前正在执行的线程对象的引用;

public final String getName()

返回该线程的名称。

 2.2.2、实现Runnable接口

Runnable接口应该有那些打算通过某一线程执行其实例的类来创建,类必须定义并实现run方法,Runnable接口语法格式如下所示:

public interface Runnable

通过实现Runnable接口创建并启动线程的步骤有三步,如下所示:

(1)定义Runnable接口的实现类,并重写该接口的run方法,该方法的方法体是线程的执行体;run方法的格式如下所示:

void run()

(2)创建Runnable实现类的实例,并以此实例创建Thread对象;

(3)调用线程对象的start方法来启动该线程;Java虚拟机调用线程的run方法;start方法的格式如下所示:

public void start()

示例代码如下所示:

public class SecondThread implements Runnable{private int i;public void run(){for (; i < 100; i++){//当线程类实现Runnable接口时,如果想要获取当前线程,只能使用Thread.currentThread()方法System.out.println(Thread.currentThread().getName() + " " + i);}}public static void main(String[] args){for (int i = 0; i < 100; i++){System.out.println(Thread.currentThread().getName() + "" + i);if (i == 20){SecondThread st = new SecondThread();// 创建并启动第一个线程new Thread(st , "新线程1").start();new Thread(st , "新线程2").start();}}}}

2.2.3、使用CallableFuture创建

Callable接口返回接口并且可能抛出异常的任务,实现者定义了一个不带任何参数的call方法;Callable接口语法格式如下所示:

public interface Callable<V>

Future接口表示异步计算的结果;Future接口语法格式如下所示:

public interface Future<V>

FutureTask类表示可取消的异步计算,利用开始和取消计算的方法、查询计算是否实现的方法和获取计算结果的方法,此类提供了对Future接口的基本实现。FutureTask类语法格式如下所示:

public class FutureTask<V>extends Objectimplements RunnableFuture<V>

使用CallableFuture创建并启动有返回值的步骤有四步,如下所示:

(1)创建Callable接口的实现类,并实现call方法,该call方法将作为线程的执行体,且该call方法有返回值;call方法的格式如下所示:

V call() throws Exception

(2)创建Callable实现类的实例,使用FutureTask类来包装Calllable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程;

(4)调用FutureTask对象的get方法来获得子线程执行结束后的返回值;get方法的格式如下所示:

V get(long timeout, TimeUnit unit)throws InterruptedException,ExecutionException,TimeoutException

示例代码如下所示:

public class ThirdThread implements Callable<Integer>{public static void main(String[] args){// 创建Callable对象ThirdThread tt = new ThirdThread();// 使用FutureTask来包装Callable对象FutureTask<Integer> task = new FutureTask<Integer>(tt);for (int i = 0; i < 100; i++){System.out.println(Thread.currentThread().getName() + " 的循环变量值为: " + i);if (i == 20){// 实质是以Callable对象来创建并启动线程new Thread(task, "有返回值线程").start();}}try{// 获取线程返回值System.out.println("子线程的返回值: " + task.get());}catch (InterruptedException e){e.printStackTrace();}catch (ExecutionException e){e.printStackTrace();}}// 实现call方法,作为线程执行体public Integer call() throws Exception{int i = 0;for (; i < 100; i++){System.out.println(Thread.currentThread().getName() + "的循环变量值为:" + i);}return i;}}

2.2.4、三者的比较

(1)通过这三种方式都可以实现多线程,而通过实现接口的两种方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常;

(2)使用实现RunnableCallable接口的方式创建多线程时,可以除实现这两个接口,还可以继承其他类;

更好的体现面向对象思想,但是如果需要访问当前线程,则必须使用Thread.currentThread()方法;

(3)使用继承Thread类创建多线程时,不可以再继承其它父类;编写简单,如果需要访问当前线程,不需要使用Thread.currentThread()方法,而是直接使用this关键字便可获得但钱线程。

2.3、线程的生命周期

当线程被创建并启动时,便表示是开始了线程的生命周期,它要经过新建、就绪、运行、阻塞以及死亡5种状态,具体的生命周期如下图所示:


2.3.1、新建和就绪状态

当程序使用new关键字创建了一个线程以后,该线程处于新建状态;当线程对象调用了start方法后,该线程就是处于就绪状态,Java虚拟机会为其创建方法调用站和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。

如果使用run方法,则run方法就会立即被执行,而且在run方法返回之前其他线程五福并发执行。

注意:startrun方法的区别?

(1)调用start方法可以启动线程,而run方法只是Thread类中的一个普通方法,调用run方法不能实现多线程;

(2)start方法:

a) Start方法用来启动线程,实现多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码;

b) 通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行;

c) 一旦得到cpu的执行权,就开始执行run方法,这里run方法称为县城提,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止;

(3)Run方法:

a) Run方法只是Thread类的一个普通方法,如果直接调用run方法,程序中依然只有主线程这个线程,其程序执行路径还是只有一条,要鞥带run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。

2.3.2、运行和阻塞状态

当发生如下几种情况,线程将会进入阻塞状态:

(1)线程调用sleep方法主动放弃所占用的处理器资源;

(2)线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;

(3)线程试图获得一个同步监测器,但该同步监测器正被其它线程所持有;

(4)线程在等待某个通知(notify);

(5)程序调用了线程的suspend方法将线程挂起了。

当前正在执行的线程被阻塞之后,其他线程便可以获得执行的机会了;被阻塞的进程会在合适的时候重新进入到就绪状态,注意是就绪状态而不是运行状态,也就是说被阻塞线程的阻塞解除后,必须等待线程调度器再次调度它。

与以上五种情况相对应,当发生如下五种特定的情况将可以解除上面的阻塞,让该线程重新进入到就绪状态:

(1)调用sleep方法的线程经过了指定时间;

(2)线程调用的阻塞式IO方法已经返回;

(3)线程成功地获得了失去取得同步监视器;

(4)线程正在等待某个通知时,其他线程发出了一个通知;

(5)处于关起状态的线程被调用了resume恢复方法。

2.3.3、线程死亡

线程会以如下三种方式来结束,结束后便处于死亡状态。

(1)run()方法执行完成,线程正常结束;

(2)线程抛出一个未捕获的Exception或者Error

(3)直接调用该线程的stop方法来结束该线程——该方法容易导致死锁。

通过调用线程对象的isAlive方法,方法格式如下所示:

public final boolean isAlive()

可以判断线程是否处于活动状态,当线程处于就绪、运行、阻塞三种状态时,该方法将返回true,当线程处于新建、死亡两种状态时,该方法将返回false

2.4、控制线程方法

2.4.1join线程

Thread类提供了join方法实现了让一个线程等待另一个线程完成的方法,join方法一共有三种重载的形式:

public final void join() throws InterruptedException

等待被join的线程执行完成;

public final void join(long millis) throws InterruptedException

等待被join的线程的时间最长为millis毫秒,如果在millis毫秒内,被join的线程还没有执行结束则不再等待;

public final void join(long millis, int nanos) throws InterruptedException

等待被join的线程的时间最长为millis毫秒加上nanos微秒(千分之一微秒)。

示例代码如下所示:

public class JoinThread extends Thread{// 提供一个有参数的构造器,用于设置该线程的名字public JoinThread(String name){super(name);}// 重写run方法,定义线程执行体public void run(){for (int i = 0; i < 100; i++){System.out.println(getName() + "-->" + i);}}public static void main(String[] args) throws InterruptedException{// 启动子线程new JoinThread("新线程").start();for (int i = 0; i < 100; i++){if (i == 20){JoinThread jt = new JoinThread("被Join的线程");jt.start();// main线程调用了jt线程的join方法,main线程必须等jt执行结束才会向下执行jt.join();}System.out.println(Thread.currentThread().getName() + "-->" + i);}}}

2.4.2、后台线程

当一个线程是在后台运行的,并且它的任务是为其他的线程提供服务,这种线程被称为“后台线程”,其特征是如果所有的前台线程都死亡,那么后台线程会自动死亡。

可以通过调用Thread对象的isDaemon方法来测试该线程是否为后台线程,其方法格式如下所示:

public final boolean isDaemon()

可以通过调用Thread对象的setDaemon()方法可将指定的线程设置为后台线程或取消;当正在运行的线程都是后台线程时,Java虚拟机退出。其方法格式如下所示:

public final void setDaemon(boolean on)

注意:当前台线程死亡后,JVM会通知后台线程死亡。将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说setDaemon(true)方发必须在start()方法之前调用。否则将会引发IllegalThreadStateException异常。

 2.4.3、线程睡眠

如果需要将正在执行的线程暂停一段时间,并进入阻塞状态,则可以调用Thread对象的sleep方法,该方法有两种重载形式:

public static void sleep(long millis)  throws InterruptedException

让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法收到系统计时器和线程调度器的精度和准确度的影响;

public static void sleep(long millis,int nanos)throws InterruptedException

让当前正在执行的线程暂停millis毫秒加nanos毫秒,并进入阻塞状态,该方法收到系统计时器和线程调度器的精度和准确度的影响;

示例代码如下所示:

public class TestSleep{public static void main(String[] args) throws InterruptedException{for (int i = 0; i < 10; i++){System.out.println("当前系统时间:" + new Date());//调用sleep方法让当前线程暂停1sThread.sleep(1000);}}}

注意:sleepwait方法的区别?

(1)这两个方法来自不同的类,sleep方法来自Thread类,而wait方法来自Object类;

(2)SleepThread类的静态类方法,水滴起哦用谁就去睡觉,即使在a线程里调用了bsleep方法,实际上还是a去睡觉,要让b线程睡觉就要在b的代码块中调用sleep

wait()Object类的非静态方法;

(3)Sleep释放资源不释放锁,而wait释放资源释放锁;

(4)使用范围:waitnotifynotifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。

 2.4.4、线程让步

sleep方法相似,它可以让当前执行的线程暂停,并执行其它线程,但他不会阻塞给线程。其方法格式如下所示:

public static void yield()

示例代码如下所示:

public class TestYield extends Thread{public TestYield(){}public TestYield(String name){super(name);}//定义run方法作为线程执行体public void run(){for (int i = 0; i < 30; i++){System.out.println(getName() + "-->" + i);//当i等于20时候,使用yield方法让当前线程让步if(i == 20){Thread.yield();}}}public static void main(String[] args){//启动两条并发线程TestYield ty1 = new TestYield("high");ty1.start();TestYield ty2 = new TestYield("low");ty2.start();}}

注意:sleep方法和yield方法的区别如下所示:

(1)sleep方法暂停当前线程后,会给其他线程执行机会,不需要理会其他线程的优先级,但yield方法只会给优先级相同或者优先级更高的线程执行机会;

(2)Sleep方法将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。而yield方法不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态;

(3)Sleep方法声明抛出了IterruptedException异常,因此调用sleep方法时需要捕捉该异常或者抛出该异常。而yield方法则没有声明抛出任何异常。

(4)Sleep方法比yield方法有更好的可移植性,通常不要依靠yield来控制并发线程的执行。

2.4.5、线程优先级

Thread提供了setPriority()getPriority()来设置和返回指定线程的优先级,其中setPriority方法的参数可以是一个整数(010之间)也可以是Thread类的三个静态常量之一,这两个方法的格式以及三个静态常量如下所示:

public final void setPriority(int newPriority)public final int getPriority()
public static final int MIN_PRIORITY线程可以具有的最低优先级public static final int NORM_PRIORITY分配给线程的默认优先级public static final int NORM_PRIORITY线程可以具有的最高优先级

2.5、线程同步

2.5.1、线程安全问题

当多个线程使用同一个数据的时候,容易会出现线程安全问题。最经典的案例便是银行取钱问题,示例代码如下所示:

public class Account{//封装账户编号、账户余额两个属性private String accountNo;private double balance;public Account(){}//有参构造器public Account(String accountNo, double balance){super();this.accountNo = accountNo;this.balance = balance;}//重新些Account的hashCode和equals方法public int hashCode(){return accountNo.hashCode();}public boolean equals(Object obj){if (obj !=null && obj.getClass() == Account.class){Account target = (Account) obj;return target.getAccountNo().equals(accountNo);}return false;}//accountNo和balance两个属性的setter和getter方法......}
public class DrawThread extends Thread{//模拟用户账户private Account account;//当前取钱线程所取得的签署private double drawAmount;public DrawThread(String name,Account account,double drawAmount){super(name);this.account = account;this.drawAmount = drawAmount;}public void run(){//当账户余额大于取钱数目if (account.getBalance() >= drawAmount){System.out.println(getName() + "成功取出: " + drawAmount  + " 元!");//修改余额account.setBalance(account.getBalance() - drawAmount);System.out.println("账户余额为:" + account.getBalance() + " 元!");}else{System.out.println(getName() + "账户余额不足!");}}}
public class TestDraw{public static void main(String[] args){//创建一个账户Account account = new Account("itheima",10000);new DrawThread("老方",account,8000).start();new DrawThread("老毕",account,6000).start();}}

以上程序运行会出现如下情况:

由上图可以清晰的看出当多个线程共享同一个数据时,程序会运行出错!

 

2.5.2synchronized

为了解决如上的问题,Java的多线程引入了同步监视器来解决这个问题,同步监视器分为同步代码块和同步方法两种。

同步的前提:

1)必须保证有两个以上线程;

2)必须是多个线程使用同一个锁,即多条语句在操作线程共享数据;

3)必须保证同步中只有一个线程在执行。

同步的好处和弊端:

1)好处:同步解决了多线程的安全问题;

2)弊端:多线程都是需要判断的,比较消耗资源。

(1)同步代码块

语法格式如下所示:

synchronized(obj){//代码}

任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视器的锁定。

对上例修改代码如下所示:

public void run(){// 使用Account作为同步监视器,任何线程进入下面同步代码块之前都得鲜活的对Account账户的锁定,//这种做法符合:加锁-->修改完成-->释放锁的逻辑synchronized (account){// 当账户余额大于取钱数目if (account.getBalance() >= drawAmount){System.out.println(getName() + "成功取出: " + drawAmount + " 元!");// 修改余额account.setBalance(account.getBalance() - drawAmount);System.out.println("账户余额为:" + account.getBalance() + " 元!");}else{System.out.println(getName() + "账户余额不足!");}}}

(1)同步方法

使用synchronized关键字来修饰某一种方法,则该种方法称为同步方法。对于同步方法而言,无需显式指定同步监视器,同步方法的同步监视器是this,即该对象本身。

使用同步方法可以将某个类编程线程安全的类,线程安全的类具有以下特征:

1) 该类的对象可以被多个线程安全的访问;

2) 每个线程调用该对象的任意方法之后都将得到正确的结果;

3) 每个线程调用该对象的任意方法之后,该对象状态依然保持合理的状态。

对上例修改代码如下所示:

public class Account{// 封装账户编号、账户余额两个属性private String accountNo;private double balance;public Account(){}// 有参构造器public Account(String accountNo, double balance){super();this.accountNo = accountNo;this.balance = balance;}// balance属性的getter方法public double getBalance(){return this.balance;}// 提供一个线程安全draw方法来完成取钱操作public synchronized void draw(double drawAmount){// 当账户余额大于取钱数目if (balance >= drawAmount){System.out.println(Thread.currentThread().getName() + "成功取出: " + drawAmount + " 元!");try{Thread.sleep(1);}catch (InterruptedException e){e.printStackTrace();}// 修改余额balance = balance - drawAmount;System.out.println("账户余额为:" + balance + " 元!");}else{System.out.println(Thread.currentThread().getName() + "账户余额不足!");}}// accountNo属性的setter和getter方法    ......// 重新Account的hashCode和equals方法......}
public class DrawThread extends Thread{//模拟用户账户private Account account;private double drawAmount;public DrawThread(String name,Account account,double drawAmount){super(name);this.account = account;this.drawAmount = drawAmount;}public void run(){//直接调用account对象draw方法执行取钱account.draw(drawAmount);}}

程序运行结果如下所示:

在以下四种情况下线程会释放对同步监视器的锁定:

(1)当前线程的同步方法或者同步代码块执行结束,当前线程即释放同步监视器;

(2)当线程在同步代码块或者同步方法中遇到breakreturn终止了该代码块或者该方法的执行,当前线程将会释放同步监视器;

(3)当线程在同步代码块或者同步方法中出现未处理的Error或者Exception时,导致了该代码块或者该方法异常结束时后将会释放同步监视器;

(4)当线程执行同步代码块或者同步方法时,程序执行了同步监视器对象的wait方法,则当前线程暂停,并释放同步监视器;

在以下两种情况下线程不会释放同步监视器:

(1)线程执行同步代码块或者同步方法时,程序调用Thread.sleep()或者Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器;

(2)线程执行同步代码块时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。

 

2.5.3、同步锁

JDK1.5之后,Java提供了另外一种线程同步机制:它通过显式定义同步锁对象来实现同步,同步锁应该使用Lock对象来充当。Lock接口的语法格式如下所示:

public interface Lock

Lock实现提供了比使用synchronized方法和语句可获得更广泛的锁定操作。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源先获得Lock对象。在实现线程安全的控制当中,通常使用ReentrantLock(可重入锁),该类的语法格式如下所示:

public class ReentrantLockextends Objectimplements Lock, Serializable

使用Lock对象可以显示加锁和释放锁,通常使用Lock对象的格式如下所示:

class X{//定义锁对象private final ReentrantLock lock = new ReentrantLock();//定义需要保证线程安全的方法public void m(){//加锁lock.lock();try{//需要保证线程安全的代码}catch (Exception e){// 需要处理的异常}//使用finally块来保证释放锁finally{lock.unlock();}}}

将上述案例修改后代码如下所示:

public class Account{//定义所对象private final ReentrantLock lock = new ReentrantLock();// 封装账户编号、账户余额两个属性......// 有参构造器     ......// balance属性的getter方法......// 提供一个线程安全draw方法来完成取钱操作public  void draw(double drawAmount){//对同步锁进行加锁lock.lock();try{// 当账户余额大于取钱数目if (balance >= drawAmount){System.out.println(Thread.currentThread().getName() + "成功取出: " + drawAmount + " 元!");try{Thread.sleep(1);}catch (InterruptedException e){e.printStackTrace();}// 修改余额balance = balance - drawAmount;System.out.println("账户余额为:" + balance + " 元!");}else{System.out.println(Thread.currentThread().getName() + "账户余额不足!");}}//使用finally块来保证释放锁finally{lock.unlock();}}// accountNo属性的setter和getter方法    ......// 重新Account的hashCode和equals方法    .......}

2.6、线程通信

2.6.1、传统的线程通信 

关于创建线程的三种方式请参阅2.2线程的创建与启动。

在定时器的应用中需要用到两个类,其中Timer类的语法格式如下所示:

public class Timer  extends Object

Timer类代表着一种工具,线程用其安排在后台线程中执行的任务,可安排任务执行一次,或这定义重复执行。

Timer类中关于schedule方法有三种重载方式:

public void schedule(TimerTask task,Date time) 表示安排在指定的时间指定指定的任务。如果此时间已经过去,则安排立即执行任务;public void schedule(TimerTask task,long delay,long period)表示安排指定的任务从指定的延迟后开始进行重复的固定延迟执行;public void schedule(TimerTask task,Date firstTime,long period)表示安排指定的任务在指定的时间开始进行重复的固定延迟执行。

TimerTask类表示由Timer安排为一次执行或者重复执行的任务。TimerTask类的语法格式如下所示:

public abstract class TimerTaskextends Objectimplements Runnable  

在程序中需要创建类实现TimerTask接口,并且重写run方法,其中run方法格式如下所示:

public abstract void run():此计时器任务要执行的操作。

示例代码如下所示:

public class TraditionalTimerTest{private static int count = 0;public static void main(String[] args){class MyTimerTask extends TimerTask{@Overridepublic void run(){count = (count + 1) % 2;System.out.println("bombing !");new Timer().schedule(new MyTimerTask(), 2000 + 2000 * count);}}new Timer().schedule(new MyTimerTask(), 2000);while (true){System.out.println(new Date().getSeconds());try{Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}}}}

2.6.2、读写锁

读写锁维护了一对相关的锁,一个用于只读操作,一个用于写入操作。只要没有Writer,读取锁可以由多个Reader线程同时保持。而且写入锁是独占的。读写锁的语法格式如下所示:

public interface ReadWriteLock

在该接口中有两个重要的方法,如下所示:

Lock readLock() 表示返回用于读取操作的锁;Lock writeLock() 表示用于写入操作的锁。

示例代码如下所示:

public class ReadWriteLockTest{public static void main(String[] args){final Queue3 q3 = new Queue3();for (int i = 0; i < 3; i++){new Thread(){public void run(){while (true){q3.get();}}}.start();new Thread(){public void run(){while (true){q3.put(new Random().nextInt(10000));}}}.start();}}}class Queue3{private Object data = null;// 共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。ReadWriteLock rwl = new ReentrantReadWriteLock();public void get(){          //给读取操作的锁加锁rwl.readLock().lock();try{System.out.println(Thread.currentThread().getName() + " be ready to read data!");Thread.sleep((long) (Math.random() * 1000));System.out.println(Thread.currentThread().getName() + "have read data :" + data);}catch (InterruptedException e){e.printStackTrace();}finally{              //给读取操作的锁解除锁rwl.readLock().unlock();}}public void put(Object data){//写锁rwl.writeLock().lock();try{System.out.println(Thread.currentThread().getName() + " be ready to write data!");Thread.sleep((long) (Math.random() * 1000));this.data = data;System.out.println(Thread.currentThread().getName() + " have write data: " + data);}catch (InterruptedException e){e.printStackTrace();}finally{rwl.writeLock().unlock();}}}

2.6.3、使用Condition控制

Condition将同步监视器方法分解成截然不同的对象,以便通过这些对象与Lock对象组合使用,为每个对象提供多个等待集。Condition接口的语法格式如下所示:

public interface Condition

Condition实例被绑定在一个Lock对象上。可以通过调用Lock对象的newCondition()方法即可。Condition类提供如下三个方法:

void await() throws InterruptedException  表示当前线程等待,知道其他线程调用该Condition的signal()方法或者signalAll()方法来唤醒该线程。void signal()  表示在此Lock对象上等待的单个线程。void signalAll()  表示唤醒在此Lock对象上等待的所有线程。

示例代码如下所示:

public class ConditionCommunication{public static void main(String[] args){final Business business = new Business();new Thread(new Runnable(){@Overridepublic void run(){for (int i = 0; i <= 50; i++){business.sub(i);}}}).start();for (int i = 0; i <= 50; i++){business.main(i);}}static class Business{private boolean bShouldSub = true;Lock lock = new ReentrantLock();Condition condition = lock.newCondition();public void sub(int i){lock.lock();try{while (!bShouldSub){try{condition.await();}catch (InterruptedException e){e.printStackTrace();}}for (int j = 0; j <= 10; j++){System.out.println("sub thread sequence of " + j + ",loop of " + i);}bShouldSub = false;// 通知主线程condition.signal();}finally{lock.unlock();}}public void main(int i){lock.lock();try{while (bShouldSub){try{condition.await();}catch (InterruptedException e){e.printStackTrace();}}for (int j = 0; j <= 10; j++){System.out.println("main thread sequence of " + j + ",loop of " + i);}bShouldSub = true;// 通知子线程condition.signal();}finally{lock.unlock();}}}}

2.6.4、使用阻塞队列控制

BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素,如果该队列已经满了,则该线程被阻塞;当消费者线程试图同BlockingQueue中取出元素时候,如果该队列已经空了,则该线程被阻塞。BlockingQueue接口语法格式如下所示:

public interface BlockingQueue<E> extends Queue<E>

以下代码是基于典型的生产者-使用者场景的一个用力。注意,BlockingQueue可以安全地与多个生产者和多个使用者一起使用。

class Producer implements Runnable{private final BlockingQueue queue;Producer(BlockingQueue q){queue = q;}public void run() {     try {       while(true) { queue.put(produce()); }     } catch (InterruptedException ex) { ... handle ...}   }Object produce() { ... }}class Consumer implements Runnable{private final BlockingQueue queue;Consumer(BlockingQueue q){queue = q;}public void run() {     try {       while(true) { consume(queue.take()); }     } catch (InterruptedException ex) { ... handle ...}   }void consume(Object x) { ... }}class Setup{void main(){BlockingQueue q = new SomeQueueImplementation();Producer p = new Producer(q);Consumer c1 = new Consumer(q);Consumer c2 = new Consumer(q);new Thread(p).start();new Thread(c1).start();new Thread(c2).start();}}

从以上的代码中可以看出该接口提供两个支持阻塞的方法:

void put(E e) throws InterruptedException  表示尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程;E take() throws InterruptedException  表示尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

以下代表表示用3个空间的队列来演示阻塞队列的功能和效果。

public class BlockingQueueTest{public static void main(String[] args){final BlockingQueue queue = new ArrayBlockingQueue(3);for (int i = 0; i < 2; i++){new Thread(){public void run(){while (true){try{Thread.sleep((long) (Math.random() * 1000));System.out.println(Thread.currentThread().getName() + "准备放数据!");queue.put(1);System.out.println(Thread.currentThread().getName() + "已经放了数据," + "队列目前有" + queue.size()+ "个数据");}catch (InterruptedException e){e.printStackTrace();}}}}.start();}new Thread(){public void run(){while (true){try{// 将此处的睡眠时间分别改为100和1000,观察运行结果Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + "准备取数据!");queue.take();System.out.println(Thread.currentThread().getName() + "已经取走数据," + "队列目前有" + queue.size()+ "个数据");}catch (InterruptedException e){e.printStackTrace();}}}}.start();}}

2.6.5、多线程的单例设计模式

保证某个类中只有一个对象。

(1)饿汉式:

class Single {//将构造函数私有化,不让别的类建立该类对象private Single(){}//自己建议一个对象private static final Single s = new Single();//提供一个公共访问方式public static Single getInstance(){return s;}}

(2)懒汉式:

class Single {//将构造函数私有化,不让别的类建立该类对象private Single(){}//提供一个公共访问方式public static Single getInstance(){if (s == null){s = new Single();}return s;}}

(3)饿汉式和懒汉式之间的区别:

a) 懒汉式是类一加载进内存就创建好对象;而懒汉式则是在类加载进内存的时候,对象还没有存在,只是在调用了getInstance()方法时,对象才开始创建。

b) 懒汉式是延迟加载,如果多个线程同时操作懒汉式时就有可能出现线程安全问题,解决线程安全问题可以加同步来解决。但是加了同步之后,每一次都要比较锁,效率因此降低,所以可以加双重判断来提高程序的效率。

将上述懒汉式的getInstance函数改写成同步:

class Single {//将构造函数私有化,不让别的类建立该类对象private Single(){}//提供一个公共访问方式public static Single getInstance(){if (s == null){synchronized(Single.class){if (s == null){s = new Single();}}}return s;}}

2.7、线程池&线程相关类

2.7.1Java5实现的线程池

当程序中需要创建大量生存期很短暂的线程是,应该使用线程池,这样可以很好的提高性能。除此之外,使用线程池可以有效的控制系统中并发线程的数量。在Java 5以后提供了Executors类产生线程池,Executors类的语法格式如下所示:

public class Executors extends Object

该工厂类包含了如下几个静态工厂方法来创建线程池:

public static ExecutorService newCachedThreadPool()  表示创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中;public static ExecutorService newFixedThreadPool(int nThreads)  表示创建一个可重用的、具有固定线程数的线程池。public static ExecutorService newSingleThreadExecutor()  表示创建一个只有单线程的线程池,相当于调用newFixedThreadPool(int nThreads)时nThreads的值为1;public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)  创建具有指定线程数的线程池,它可以在指定言辞后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也是保存在线程池内。public static ScheduledExecutorService newSingleThreadScheduledExecutor()  表示创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。

使用线程池来执行线程任务的步骤如下所示:

(1)调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。

(2)创建Runnable实现类或者Callable实现类的实例,作为线程执行任务。

(3)调用ExecutorService对象的submit方法来提交Runnable实例或者Callable实例;

(4)当不想提交任何任务时,调用ExecutorService对象所代表的任务。

以下程序代表使用线程池来执行指定Runnable对象所代表的任务。

//实现Runnable接口来定义一个简单的线程类class MyThread implements Runnable{public void run(){for (int i = 0; i < 100; i++){System.out.println(Thread.currentThread().getName() + "的i值为:" + i);}}}public class ThreadPoolTest{public static void main(String[] args) throws Exception{// 创建一个具有固定线程数的线程池ExecutorService pool = Executors.newFixedThreadPool(6);// 想线程池中提交两个线程pool.submit(new MyThread());pool.submit(new MyThread());// 关闭线程池pool.shutdown();}}

以下代码实现固定大小的线程池和缓存线程池,实现步骤如下所示:

1)用3个大小的固定线程池去执行10个内部循环10次就结束的任务,打印出正在执行的线程名、任务序号以及任务内部的循环次数,这10个任务要用个子独立的Runnable对象,才能看到任务的序号;

2)该为缓存线程池,可以看到当前有多少个任务,就会分配多少个线程为之服务。

示例代码如下所示:

public class ThreadPoolTest{public static void main(String[] args){ExecutorService threadPool = Executors.newSingleThreadExecutor();for (int i = 0; i < 10; i++){final int task = i;threadPool.execute(new Runnable(){@Overridepublic void run(){for (int j = 0; j < 10; j++){try{Thread.sleep(20);}catch (InterruptedException e){e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " is looping of " + j + " for task of "+ task);}}});}System.out.println("all of 3 tasks have committed!");Executors.newScheduledThreadPool(3).scheduleAtFixedRate(new Runnable(){@Overridepublic void run(){System.out.println("bombing");}}, 6, 2, TimeUnit.SECONDS);}}

2.7.2Java7实现ForkJoinPool

为了充分利用利用多CPU的性能优势,可以考虑把一个任务拆分成多个“小任务”,然后将多个“小任务”放到多个处理器核心上并行执行。Java 7中提供了ForkJoin类实现此功能,ForkJoinPoolExecutorService类的实现类,具体语法格式如下所示:

public class ForkJoinPoolextends AbstractExecutorService

ForkJoin中提供了如下两个常用的构造器:

public ForkJoinPool(int parallelism)  表示创建一个包含parallelism个并行线程的ForkJoinPool。public ForkJoinPool()  表示以Runtime.availableProcessors()方法的返回值作为parallelism参数来创建ForkJoinPool。

创建了ForkJoinPool实例之后,就可以调用ForkJoinPoolsubmit(ForkJoinTask<T> task)或者invoke(ForkJoinTask<T> task)方法来执行指定的任务。其中ForkJoinTask代表一个可以并行、合并的任务。

上图显示了ForkJoinPoolForkJoinTask等类的类图。

public class ForkJoinPoolTest{public static void main(String[] args) throws InterruptedException{ForkJoinPool pool = new ForkJoinPool();// 提交可分解的PrintTask任务pool.submit(new PrintTask(0, 100));pool.awaitTermination(2, TimeUnit.SECONDS);// 关闭线程池pool.shutdown();}}class PrintTask extends RecursiveAction{// 每个小任务最多打印50个数private static final int THRESHOLD = 50;private int start;private int end;// 打印从start到end的任务public PrintTask(int start, int end){this.start = start;this.end = end;}@Overrideprotected void compute(){// 当end与start之间的差小于THRESHOLD,开始 打印if (end - start < THRESHOLD){for (int i = start; i < end; i++){System.out.println(Thread.currentThread().getName() + " 的i值为:  " + i);}}else{// 当end与start之间的差大于THRESHOLD;即要打印的数超过50个时,将大任务分解成两个小任务int middle = (start + end) / 2;PrintTask left = new PrintTask(start, middle);PrintTask right = new PrintTask(middle, end);// 并行执行两个小任务left.fork();right.fork();}}}

在本人计算机上运行结果如下所示:

上述程序定义的任务是一个没有返回值的打印任务,如果大任务有返回值的任务,可以让任务继承RecursiveTask<T>,其中个泛型参数T代表了该任务的返回值类型。

//继承RecursiveTask来实现可分解的任务class CalTask extends RecursiveTask<Integer>{//每个“小任务”最多只累加20个数private static final int THRESHOLD = 20;private int arr[];private int start;private int end;//累加从start到end的数组元素public CalTask(int []arr,int start,int end){this.arr = arr;this.start = start;this.end = end;}@Overrideprotected Integer compute(){int sum = 0;//当end 与start之间的差小于THRESHOLD时,开始进行实际累加if(end - start < THRESHOLD){for(int i = start;i<end;i++){sum += arr[i];}return sum;}else{// 当end与start之间的差大于THRESHOLD;即要打印的数超过20个时,将大任务分解成两个小任务int middle = (start + end) / 2;CalTask left = new CalTask(arr, start, middle);CalTask right = new CalTask(arr, middle, end);//并行执行两个小任务left.fork();right.fork();//把两个小任务类家的结果合并起来return left.join() + right.join();}}}public class Sum{public static void main(String[] args) throws InterruptedException, ExecutionException{int[] arr = new int[100];Random rand = new Random();int total = 0;//初始化100个数字的元素for (int i = 0; i < arr.length; i++){int tmp = rand.nextInt(20);//对数组元素赋值,并讲数组元素的值添加到total总和中total += (arr[i] = tmp);}System.out.println("total = " + total);ForkJoinPool pool = new ForkJoinPool();//提交可分解的CalTask任务Future<Integer> future = pool.submit(new CalTask(arr, 0, arr.length));System.out.println("future get = " + future.get());//关闭线程池pool.shutdown();}}

以上程序运行时结果如下所示:

2.7.3ThreadLocal

线程范围内共享数据的示意图如下所示:


以下代码可以解释线程范围内共享数据的概念。

public class ThreadScopeShareData{//全局的静态变量private static int data = 0;private static Map<Thread,Integer> threadData = new HashMap<Thread,Integer>();public static void main(String[] args){for (int i = 0; i < 2; i++){new Thread(new Runnable(){@Overridepublic void run(){//为data赋值int data = new Random().nextInt();System.out.println(Thread.currentThread().getName() + " has put data :" + data);//将data数据放入线程共享中threadData.put(Thread.currentThread(), data);//A、B模块获取共享数据new A().get();new B().get();}}).start();}}/** * A模块 * */static class A{public void get(){int data = threadData.get(Thread.currentThread());System.out.println("A from " + Thread.currentThread().getName() + " has put data :" + data);}}/** * B模块 * */static class B{public void get(){int data = threadData.get(Thread.currentThread());System.out.println("B from " + Thread.currentThread().getName() + " has put data :" + data);}}}

运行结果如下所示:

public class ThreadLocal<T>extends Object

ThreadLocal类是用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。ThreadLocal类一共提供了以下三种方法:

public T get()表示返回此线程局部变量中当前线程副本中的值。public void set(T value)表示删除此线程局部变量中当前线程的值。public void set(T value)表示设置此线程局部变量中当前线程副本中的值。

ThreadLocal类的应用举例

public class ThreadLocalTest{private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();private static ThreadLocal<MyThreadScopeData> myThreadScopeData = new ThreadLocal<MyThreadScopeData>();public static void main(String[] args){for (int i = 0; i < 2; i++){new Thread(new Runnable(){@Overridepublic void run(){int data = new Random().nextInt();System.out.println(Thread.currentThread().getName() + " has put data :" + data);x.set(data);MyThreadScopeData.getThreadInstance().setName("name" + data);MyThreadScopeData.getThreadInstance().setAge(data);//A、B模块获取共享数据new A().get();new B().get();}}).start();}}/** * A模块 * */static class A{public void get(){int data = x.get();System.out.println("A from " + Thread.currentThread().getName() + " has put data :" + data);MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();System.out.println("A from " + Thread.currentThread().getName() + " get MyData :" + myData.getName() + ","+ myData.getAge());}}/** * B模块 * */static class B{public void get(){int data = x.get();System.out.println("B from " + Thread.currentThread().getName() + " has put data :" + data);MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();System.out.println("B from " + Thread.currentThread().getName() + " get MyData :" + myData.getName() + ","+ myData.getAge());}}}class MyThreadScopeData{private MyThreadScopeData(){}public static MyThreadScopeData getThreadInstance(){MyThreadScopeData instance = map.get();if (instance == null){instance = new MyThreadScopeData();map.set(instance);}return instance;}private static MyThreadScopeData instance = null;private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();private String name;private int age;//name属性的getter和setter......//age属性的getter和setter     ......}

运行结果如下所示:

总结:一个ThreadLocal代表一个变来那个,因此其中只能放一个数据。如果有两个变量都要线程范围内共享,则要定义两个ThreadLocal对象。但如果有一百个变量需要线程共享,则先定义一个对象来封装这一百个对象,然后在ThreadLocal中存储这个对象。

2.7.4Semaphore实现信号灯

public class Semaphore extends Objectimplements Serializable

Semaphore类代表一个计数信号量。通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。例如,下面的类使用信号量控制对内容池的访问。

class Pool{private static final int MAX_AVAILABLE = 100;private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);public Object getItem() throws InterruptedException{available.acquire();return getNextAvailableItem();}public void putItem(Object x){if (markAsUnused(x)) available.release();}// Not a particularly efficient data structure; just for demoprotected Object[] items = ... whatever kinds of items being managedprotected boolean[] used = new boolean[MAX_AVAILABLE];protected synchronized Object getNextAvailableItem(){for (int i = 0; i < MAX_AVAILABLE; ++i){if (!used[i]){used[i] = true;return items[i];}}return null; // not reached}protected synchronized boolean markAsUnused(Object item){for (int i = 0; i < MAX_AVAILABLE; ++i){if (item == items[i]){if (used[i]){used[i] = false;return true;}else return false;}}return false;}}

Semaphore类有两个构造函数,格式如下所示:

public Semaphore(int permits)  表示创建具有给定的许可数和非公平设置的Semaphore。public Semaphore(int permits,boolean fair)  表示创建具有给定的许可数和给定的公平设置的Semaphore。

该类有两个重要的方法,如下所示:

public void acquire(int permits) throws InterruptedException  表示从此信号量获取给定数目的认可,在提供这些许可前一直将线程阻塞,或者线程已经被中断。获取给定数目的许可之后立即返回,将可用的许可数减去给定的量。public void release(int permits)  表示释放给定数目的许可,将其返回到信号量。

以下程序表示创建5个线程,使用Semaphore来控制可以访问的资源的线程个数为3个。

public class SemaphoreTest//信号灯测试{public static void main(String[] args){ExecutorService service = Executors.newCachedThreadPool();final Semaphore sp = new Semaphore(3);for (int i = 0; i < 5; i++){Runnable runnable = new Runnable(){public void run(){try{sp.acquire();}catch (InterruptedException e1){e1.printStackTrace();}System.out.println("线程" + Thread.currentThread().getName() + "进入,当前已有"+ (3 - sp.availablePermits()) + "个并发");try{Thread.sleep((long) (Math.random() * 10000));}catch (InterruptedException e){e.printStackTrace();}System.out.println("线程" + Thread.currentThread().getName() + "即将离开");sp.release();// 下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元System.out.println("线程" + Thread.currentThread().getName() + "已离开,当前已有"+ (3 - sp.availablePermits()) + "个并发");}};service.execute(runnable);}}}

运行的结果如下所示:



2.7.5、同步集合

Java5中提供了如下一些同步集合类:部分类如下图所示:

其余请查阅文档。

2.7.6、其它同步工具类

(1)CyclicBarrier

public class CyclicBarrierextends Object

表示大家彼此等待,大家集合好后才开始出发。分散活动后又在指定的地点集合碰面。

以下程序表示用三个线程来实现以上示例:

public class CyclicBarrierTest{public static void main(String[] args){ExecutorService service = Executors.newCachedThreadPool();final CyclicBarrier cb = new CyclicBarrier(3);for (int i = 0; i < 3; i++){Runnable runnable = new Runnable(){public void run(){try{Thread.sleep((long) (Math.random() * 10000));System.out.println("线程" + Thread.currentThread().getName() + "即将到达集合地点1,当前已有"+ (cb.getNumberWaiting() + 1) + "个已经到达,"+ (cb.getNumberWaiting() == 2 ? "都到齐了,继续走啊" : "正在等候"));cb.await();Thread.sleep((long) (Math.random() * 10000));System.out.println("线程" + Thread.currentThread().getName() + "即将到达集合地点2,当前已有"+ (cb.getNumberWaiting() + 1) + "个已经到达,"+ (cb.getNumberWaiting() == 2 ? "都到齐了,继续走啊" : "正在等候"));cb.await();Thread.sleep((long) (Math.random() * 10000));System.out.println("线程" + Thread.currentThread().getName() + "即将到达集合地点3,当前已有"+ (cb.getNumberWaiting() + 1) + "个已经到达,"+ (cb.getNumberWaiting() == 2 ? "都到齐了,继续走啊" : "正在等候"));cb.await();}catch (Exception e){e.printStackTrace();}}};service.execute(runnable);}service.shutdown();}}

运行结果如下所示:

(2)CountDownLatch

public class CountDownLatch extends Object

犹如倒计时计数器,调用CountDownLatch对象的countDown方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。

以下代表实现了如下功能:实现一个人(也可以是多个人)等待其他所有人都来通知他,可以实现一个人通知多个人的效果。具体代码如下所示:

public class CountdownLatchTest{public static void main(String[] args){ExecutorService service = Executors.newCachedThreadPool();final CountDownLatch cdOrder = new CountDownLatch(1);final CountDownLatch cdAnswer = new CountDownLatch(3);for (int i = 0; i < 3; i++){Runnable runnable = new Runnable(){public void run(){try{System.out.println("线程" + Thread.currentThread().getName() + "正准备接受命令");cdOrder.await();System.out.println("线程" + Thread.currentThread().getName() + "已接受命令");Thread.sleep((long) (Math.random() * 10000));System.out.println("线程" + Thread.currentThread().getName() + "回应命令处理结果");cdAnswer.countDown();}catch (Exception e){e.printStackTrace();}}};service.execute(runnable);}try{Thread.sleep((long) (Math.random() * 10000));System.out.println("线程" + Thread.currentThread().getName() + "即将发布命令");cdOrder.countDown();System.out.println("线程" + Thread.currentThread().getName() + "已发送命令,正在等待结果");cdAnswer.await();System.out.println("线程" + Thread.currentThread().getName() + "已收到所有响应结果");}catch (Exception e){e.printStackTrace();}service.shutdown();}}

运行结果如下所示:


(3)Exchange

public class Exchanger<V>extends Object

表示用于实现两个人之间的数据交换,每个人在完成一定的事务后想与对方交换数据,第一个先拿出数据的人将一直等待第二个人拿着数据到来时,才能彼此交换数据。

public class ExchangerTest{public static void main(String[] args){ExecutorService service = Executors.newCachedThreadPool();final Exchanger exchanger = new Exchanger();service.execute(new Runnable(){public void run(){try{String data1 = "zxx";System.out.println("线程" + Thread.currentThread().getName() + "正在把数据" + data1 + "换出去");Thread.sleep((long) (Math.random() * 10000));String data2 = (String) exchanger.exchange(data1);System.out.println("线程" + Thread.currentThread().getName() + "换回的数据为" + data2);}catch (Exception e){e.printStackTrace();}}});service.execute(new Runnable(){public void run(){try{String data1 = "lhm";System.out.println("线程" + Thread.currentThread().getName() + "正在把数据" + data1 + "换出去");Thread.sleep((long) (Math.random() * 10000));String data2 = (String) exchanger.exchange(data1);System.out.println("线程" + Thread.currentThread().getName() + "换回的数据为" + data2);}catch (Exception e){e.printStackTrace();}}});}}

运行结果如下所示:


原创粉丝点击