JavaSE(5):java多线程技术
2014-06-02 03:03
134 查看
五、 Java线程技术
1、基础概念
程序:是一段静态的代码,是人们解决问题的思维方式在计算机汇中的描述,是应用软件执行的蓝本,是一个静态的概念。
进程:是程序的一个运行例程,是程序的一次动态执行过程,进程是计算机进行资源分配的独立单位。
线程:是进程中相对独立的一个执行单元,是操作系统调度的基本单位,一个进程可以包含多个线程。进程并不执行代码,它只是代码存放的地址空间,进程地址空间中所存放的代码由线程来执行,线程的执行由操作系统负责调度。
线程的运行机制:在java中每一个线程都有一个独立的程序计数器和方法调用栈。程序计数器用来记录线程当前执行程序代码的位置,指向下一条要执行的指令。方法调用栈是用来描述线程在执行时一系列的方法调用过程,栈中每一个元素称为一个栈帧,每一个栈帧对应一个方法调用,帧中保存方法调用的参数、局部变量及执行过程中的临时数据。
2、线程的创建和启动
线程的创建有两种方法:继承Thread类、实现Runnable接口
继承Thread类:Thread类的每个实例或其子类的实例都是一个线程,故可以通过Thread类或它的派生类创建线程实例并启动一个新的线程。
自定义Thread类的派生类创建线程,主要是覆盖方法run(),在这个方法中加入线程执行代码即可。
实现Runnable接口:推荐使用这种方法,因为java类是单继承的,继承了Thread类则无法继承其他类。Runnable接口只有一个方法run()。过程如下:1、定义Runnable接口的实现类,重写该接口的run方法。2、创建Runnable实现类的实例,以该实例作为Thread类的参数创建一个线程对象,这个Thread对象就是需要的线程对象。
可见,当使用Runnable接口时,不能直接创建该接口实现类的对象作为线程,必须从Thread类的一个实例内部运行它。
CreateThread.java(两种创建线程的方法实现、及其对比)
TwoThreadPrintTogather.java(两个线程共同实现打印工作)
3、线程的启动
创建一个线程对象之后,仅仅是在内存中出现了一个线程类的实例对象,线程并不会自动开始运行,必须调用线程对象的start方法来启动线程。它实现了为线程分配必要的资源,使线程处于可以运行状态,同时调用了线程的run方法来运行线程。
注:run方法可以通过线程的start自动在新线程中执行,也可以当做一般的函数被类的对象调用,但是不会在新的线程中执行。
4、线程状态与线程控制
线程状态:线程对象被创建时,线程的生命周期开始,直到该对象被撤消为止。在这个周期中,线程并不是一创建就进入运行状态,线程启动之后,也不是一直处于运行状态,线程在生命周期内主要有以下几个状态,并可以相互装化。
创建状态(NEW)、可运行状态(Runnbale)、运行状态(Running)、阻塞状态(Blocked)、等待状态(Waiting)、计时等待(Timed Waiting)、死亡状态(Dead)。
创建状态(NEW):创建了一个线程对象而没有启动它,则会处于该状态。
可运行状态(Runnable):创建状态的线程对象调用了start方法之后,进入了可运行状态。此时JVM为其创建了方法调用栈和程序计数器。注:线程处于可运行状态只是说明其具有运行条件,不一定是正在运行中,还得取决于系统的调度,分配cpu时间片。
运行状态(Running):处在“可执行状态”的线程对象一旦获得了 CPU 控制权,就会转换到“运行状态”,在“运行状态”下,线程状态占用 CPU 时间片段,执行run 方法中的代码,处在“执行状态”下的线程可以调用 yield 方法,该方法用于主动出让 CPU 控制权。线程对象出让控制权后回到“可执行状态”,重新等待调度。
阻塞和等待状态:由线程的运行状态转变而来,使线程放弃当前处理机会(占用cpu机会),直到线程调试器重新激活它(系统调度重新让它分配到cpu上)。进入该状态有以下几种原因:
①当线程试图获取一个内部的对象锁,但是该锁被其他线程占有时,该线程进入阻塞状态。
②线程调用wait方法等待另一个线程通知,或调用join方法等待另一个线程结束,线程会进入等待状态。
③线程调用sleep、wait、join等方法时,传入了一个超时参数,线程在这些时间内进入计时等待状态。
解除阻塞的方法:睡眠状态超时、调用 join 后等待其他线程执行完毕、I/O 操作执行完毕、调用阻塞线程的 interrupt 方法(线程睡眠时,调用该线程的interrupt方法会抛出InterruptedException)。
死亡状态(Dead):run方法执行完成、线程抛出未捕获的Exception或Error、调用该线程的stop方法,这三种情况会使线程死亡,一旦死亡,不能重新工作。可以使用 Thread 类的 isAlive 方法判断线程是否活着。
ThreadStatus.java(线程状态控制,主要是sleep和join方法、interrupt方法的使用)
线程控制
Join方法:调用join方法的线程对象强制运行,该线程强制运行期间,其他线程无法运行,必须等待该线程结束后其他线程才能运行。也称联合线程。
使用流程:A线程执行过程中,调用B线程join方法,则A进入等待,B执行完之后,A恢复执行。
Sleep方法:线程休眠。A正在执行,调用A的sleep方法,A进入计时等待状态。等待计时结束后,并不会返回运行状态,而是可运行状态,所以不能保证sleep结束后会立即执行,需要依靠系统的调度。Sleep只能控制当前正在运行的线程。
Yield方法:与sleep类似,但是A调用yield方法不会使A进入阻塞状态,而是让A由运行状态返回到可运行状态,同时让系统再重新调度一下(重新分抢cpu使用权),所以,若A的优先级很高,这个方法不会让A让出执行权的。
线程优先级:(较少使用)决定线程的cpu调度优先程度。通过setPriority和getPriority设置和获取优先级。Main方法默认为NORM_PRIORITY(值为5),最大为MAX_PRIORITY,值为10,最小为MIN_PRIORITY(1)。
Daemon后台线程:处于后台运行,为其他线程提供服务,也成为守护线程,JVM的垃圾回收就是典型的后台线程。
特点:所有的前台线程都死亡,则后台线程自动死亡。
设置后台线程:setDaemon(true)方法,必须在start之前调用,否则抛异常。前台线程创建的线程默认是前台线程。判断用isDaemon方法。如果一个线程时Daemon线程,那么它创建的任何线程也会自动具备Daemon属性。
ThreadContral.java(三种控制方法的使用),见前文ThreadStatus.java
DeamonTest.java(Daemon线程的实例)
5、线程同步安全问题
多线程在访问同一个共享数据时,一个线程还没执行完,领一个线程参与进来执行,会导致共享数据的错误,所以应该避免这种情况,对共享数据操作的语句,只能让一个线程都执行完毕,其他线程不能参与执行。
三种方法解决这个问题。
①同步代码块:synchronized(obj){ },obj是一个同步对象
②同步方法:synchronized returnType methodName(paramList){ },通常不直接在run方法上添加。同步方法的同步监听器其实是this。静态方法的默认同步锁是当前方法所在类的.calss对象。
③同步锁:与synchronized类似,但是功能更强大,通常使用ReentrantLock(可重入锁)。
注:使用synchronized关键字时,避免在其方法或代码块中使用sleep或yield方法,因为该程序块占用着对象锁,其他线程只能等该线程醒来或执行完毕才能执行,严重影响效率,且逻辑不符。当然与与同步程序块无关的线程可以获得更多的运行时间。
ThreadSafty.java(同步锁的实现实例、线程同步的两个案例:账户操作例程、分苹果例程)
6、线程通信
典型的生产者与消费者问题:生产者和消费者共享存放产品的仓库,若仓库为空时,消费者无法消费,仓满时,生产者无法继续生产。Java通过wait、notify、notifyAll三个方法提供了线程的通信。这三个方法都只能在synchronized关键字范围内使用。
wait()方法:中断方法的执行,使本线程等待,暂时让出 cpu 的使用权,并允许其他线程使用这个同步方法,即同时让出对象锁,直到其他使用同一个对象锁线程调用notify方法或notifyAll方法为止。
notify()方法:唤醒由于使用这个同步方法(同步对象锁)而处于等待线程的 某一个结束等待。
notifyall()方法:唤醒所有由于使用这个同步方法(同步对象锁)而处于等待的线程结束等待。
以上三个方法是Object的方法而不是Thread的成员方法,这三个方法必须用同步对象来调用。若是synchronized同步方法,则因为该类默认的实例this就是同步对象,可以在同步方法中调用这三个方法;若是synchronized修饰的同步代码块,同步锁是括号中的对象,所以必须使用该对象调用对象的这三个方法。
若用Lock代替同步方法或同步代码块,Condition代替同步对象锁,Condition对象通过Lock对象的newCondition方法创建,await等价wait,signal等价notify,signalAll等价notifyAll。
Wait和sleep的区别:1、这两个方法来自不同的类分别是Thread和Object;2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法;3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围);4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
ThreadCommunite.java(两个实例介绍线程通信:买票实例、生产消费者实例,一个lock线程通信实例)
附重要知识:重点,线程同步安全和线程通信。
效果:前者交替随机打印。后者AB严格依次打印
1、基础概念
程序:是一段静态的代码,是人们解决问题的思维方式在计算机汇中的描述,是应用软件执行的蓝本,是一个静态的概念。
进程:是程序的一个运行例程,是程序的一次动态执行过程,进程是计算机进行资源分配的独立单位。
线程:是进程中相对独立的一个执行单元,是操作系统调度的基本单位,一个进程可以包含多个线程。进程并不执行代码,它只是代码存放的地址空间,进程地址空间中所存放的代码由线程来执行,线程的执行由操作系统负责调度。
线程的运行机制:在java中每一个线程都有一个独立的程序计数器和方法调用栈。程序计数器用来记录线程当前执行程序代码的位置,指向下一条要执行的指令。方法调用栈是用来描述线程在执行时一系列的方法调用过程,栈中每一个元素称为一个栈帧,每一个栈帧对应一个方法调用,帧中保存方法调用的参数、局部变量及执行过程中的临时数据。
2、线程的创建和启动
线程的创建有两种方法:继承Thread类、实现Runnable接口
继承Thread类:Thread类的每个实例或其子类的实例都是一个线程,故可以通过Thread类或它的派生类创建线程实例并启动一个新的线程。
自定义Thread类的派生类创建线程,主要是覆盖方法run(),在这个方法中加入线程执行代码即可。
实现Runnable接口:推荐使用这种方法,因为java类是单继承的,继承了Thread类则无法继承其他类。Runnable接口只有一个方法run()。过程如下:1、定义Runnable接口的实现类,重写该接口的run方法。2、创建Runnable实现类的实例,以该实例作为Thread类的参数创建一个线程对象,这个Thread对象就是需要的线程对象。
可见,当使用Runnable接口时,不能直接创建该接口实现类的对象作为线程,必须从Thread类的一个实例内部运行它。
CreateThread.java(两种创建线程的方法实现、及其对比)
package blog5; /** * 演示两种实现线程的方法 */ public class CreateThread { public static void main(String[] args) { MyThread exThread = new MyThread(); //Runnable实现类也必须寄托于Thread类才能创建线程 Thread runThread = new Thread(new MyRunnableThread()); exThread.start(); runThread.start(); } } class MyRunnableThread implements Runnable{ @Override public void run() { for(int j=0;j<100;j++){ System.out.println("Runnable Thread print:" + j); } System.out.println(Thread.currentThread().getName() + " is Over..."); } } class MyThread extends Thread{ public MyThread() { super(); } @Override public void run() { for(int j=0;j<100;j++){ System.out.println("Extends Thread print:" + j); } System.out.println(Thread.currentThread().getName() + " is Over..."); } }
TwoThreadPrintTogather.java(两个线程共同实现打印工作)
package blog5; /** * 两个线程共同打印0—99,但会有线程安全问题 * */ public class TwoThreadPrintTogather implements Runnable{ static int i=0; @Override public void run() { for(;i<100;i++){ System.out.println(Thread.currentThread().getName()+ ":" + i); } } public static void main(String[] args) { Thread thread1 = new Thread(new TwoThreadPrintTogather()); Thread thread2 = new Thread(new TwoThreadPrintTogather()); thread1.start(); thread2.start(); } }
3、线程的启动
创建一个线程对象之后,仅仅是在内存中出现了一个线程类的实例对象,线程并不会自动开始运行,必须调用线程对象的start方法来启动线程。它实现了为线程分配必要的资源,使线程处于可以运行状态,同时调用了线程的run方法来运行线程。
注:run方法可以通过线程的start自动在新线程中执行,也可以当做一般的函数被类的对象调用,但是不会在新的线程中执行。
4、线程状态与线程控制
线程状态:线程对象被创建时,线程的生命周期开始,直到该对象被撤消为止。在这个周期中,线程并不是一创建就进入运行状态,线程启动之后,也不是一直处于运行状态,线程在生命周期内主要有以下几个状态,并可以相互装化。
创建状态(NEW)、可运行状态(Runnbale)、运行状态(Running)、阻塞状态(Blocked)、等待状态(Waiting)、计时等待(Timed Waiting)、死亡状态(Dead)。
创建状态(NEW):创建了一个线程对象而没有启动它,则会处于该状态。
可运行状态(Runnable):创建状态的线程对象调用了start方法之后,进入了可运行状态。此时JVM为其创建了方法调用栈和程序计数器。注:线程处于可运行状态只是说明其具有运行条件,不一定是正在运行中,还得取决于系统的调度,分配cpu时间片。
运行状态(Running):处在“可执行状态”的线程对象一旦获得了 CPU 控制权,就会转换到“运行状态”,在“运行状态”下,线程状态占用 CPU 时间片段,执行run 方法中的代码,处在“执行状态”下的线程可以调用 yield 方法,该方法用于主动出让 CPU 控制权。线程对象出让控制权后回到“可执行状态”,重新等待调度。
阻塞和等待状态:由线程的运行状态转变而来,使线程放弃当前处理机会(占用cpu机会),直到线程调试器重新激活它(系统调度重新让它分配到cpu上)。进入该状态有以下几种原因:
①当线程试图获取一个内部的对象锁,但是该锁被其他线程占有时,该线程进入阻塞状态。
②线程调用wait方法等待另一个线程通知,或调用join方法等待另一个线程结束,线程会进入等待状态。
③线程调用sleep、wait、join等方法时,传入了一个超时参数,线程在这些时间内进入计时等待状态。
解除阻塞的方法:睡眠状态超时、调用 join 后等待其他线程执行完毕、I/O 操作执行完毕、调用阻塞线程的 interrupt 方法(线程睡眠时,调用该线程的interrupt方法会抛出InterruptedException)。
死亡状态(Dead):run方法执行完成、线程抛出未捕获的Exception或Error、调用该线程的stop方法,这三种情况会使线程死亡,一旦死亡,不能重新工作。可以使用 Thread 类的 isAlive 方法判断线程是否活着。
ThreadStatus.java(线程状态控制,主要是sleep和join方法、interrupt方法的使用)
package blog5; /** * 线程状态的转化练习,实现线程的控制 * 主要为sleep、yield、join、interrupt的使用 * 注意:yield和sleep的区别 */ public class ThreadStatus { public static void main(String[] args) { Thread threadA = new Thread(new MyThreadA()); threadA.setName("线程A"); Thread threadB = new Thread(new MyThreadB()); threadB.setName("线程B"); Thread threadC = new Thread(new MyThreadA()); threadC.setName("线程C"); threadA.start(); threadB.start(); threadC.start(); for(int i=0;i<30;i++){ if(i==5){ System.out.println("主线程打印到5啦,把线程B的睡眠中断吧,让它也有机会打印..."); threadB.interrupt(); } if(i==0){ System.out.println("先让主线程睡眠,让A、C线程打印吧(B也在睡眠中)..."); try { Thread.sleep(10); } catch (InterruptedException e) { } } if(i==10){ System.out.println("主线程打印到10了,别打印了,先让B打印完吧"); try { threadB.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("here is main thread..." + i); Thread.yield();//主线程每打印完一次,让所有活线程重新抢cpu } } } class MyThreadA implements Runnable{ int i = 1; public void run() { for(;i<=20;i++){ System.out.println(Thread.currentThread().getName() + ":" + i); Thread.yield();//打印完一次,让所有活线程重新抢cpu } } } class MyThreadB extends Thread{ public void run() { try { sleep(100000); } catch (InterruptedException e) { System.out.println("睡眠被中断了"); } for(int i=0;i<20;i++){ System.out.println("中断睡醒打印:"+i); } } }
线程控制
Join方法:调用join方法的线程对象强制运行,该线程强制运行期间,其他线程无法运行,必须等待该线程结束后其他线程才能运行。也称联合线程。
使用流程:A线程执行过程中,调用B线程join方法,则A进入等待,B执行完之后,A恢复执行。
Sleep方法:线程休眠。A正在执行,调用A的sleep方法,A进入计时等待状态。等待计时结束后,并不会返回运行状态,而是可运行状态,所以不能保证sleep结束后会立即执行,需要依靠系统的调度。Sleep只能控制当前正在运行的线程。
Yield方法:与sleep类似,但是A调用yield方法不会使A进入阻塞状态,而是让A由运行状态返回到可运行状态,同时让系统再重新调度一下(重新分抢cpu使用权),所以,若A的优先级很高,这个方法不会让A让出执行权的。
线程优先级:(较少使用)决定线程的cpu调度优先程度。通过setPriority和getPriority设置和获取优先级。Main方法默认为NORM_PRIORITY(值为5),最大为MAX_PRIORITY,值为10,最小为MIN_PRIORITY(1)。
Daemon后台线程:处于后台运行,为其他线程提供服务,也成为守护线程,JVM的垃圾回收就是典型的后台线程。
特点:所有的前台线程都死亡,则后台线程自动死亡。
设置后台线程:setDaemon(true)方法,必须在start之前调用,否则抛异常。前台线程创建的线程默认是前台线程。判断用isDaemon方法。如果一个线程时Daemon线程,那么它创建的任何线程也会自动具备Daemon属性。
ThreadContral.java(三种控制方法的使用),见前文ThreadStatus.java
DeamonTest.java(Daemon线程的实例)
package blog5; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; /** * 守护线程实例 * 流程:当用户输入yes,主线程创建一个守护线程,主线程开始睡眠10ms,此时间内,守护线程输出字符, * 但是,当主线程睡眠结束,不管守护线程run方法完毕否(输出还没结束),程序都会终止。 * 当用户输入非yes,主线程创建一个用户线程,然后进入睡眠10ms,不管主线程睡眠完毕否,用户线程会一直执行 * 其run方法,知道用户线程也结束了,程序才结束。 * 所以,有些IO操作不要放在Daemon线程中,会造成资源未输出就已经关闭了程序。 */ public class DaemonTest { public static void main(String[] args) throws IOException, InterruptedException { System.out.println("Thread's daemon status,yes or no:"); // 建立缓冲字符输入流 BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); String str; // 从键盘读取一个字符串 str = stdin.readLine(); //异常抛出不处理 if(str.equals("yes")){ //创建守护线程,并启动 ThreadTest t = new ThreadTest(); t.setDaemon(true); t.start(); }else{ new ThreadTest().start(); //创建用户线程并启动 } Thread.sleep(10); } } class ThreadTest extends Thread { @Override public void run() { // 输出当前线程是否为后台线程 for (int i = 0; i < 100; i++) { System.out.println("NO. " + i + " Daemon is " + isDaemon()); try { sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } }
5、线程同步安全问题
多线程在访问同一个共享数据时,一个线程还没执行完,领一个线程参与进来执行,会导致共享数据的错误,所以应该避免这种情况,对共享数据操作的语句,只能让一个线程都执行完毕,其他线程不能参与执行。
三种方法解决这个问题。
①同步代码块:synchronized(obj){ },obj是一个同步对象
②同步方法:synchronized returnType methodName(paramList){ },通常不直接在run方法上添加。同步方法的同步监听器其实是this。静态方法的默认同步锁是当前方法所在类的.calss对象。
③同步锁:与synchronized类似,但是功能更强大,通常使用ReentrantLock(可重入锁)。
注:使用synchronized关键字时,避免在其方法或代码块中使用sleep或yield方法,因为该程序块占用着对象锁,其他线程只能等该线程醒来或执行完毕才能执行,严重影响效率,且逻辑不符。当然与与同步程序块无关的线程可以获得更多的运行时间。
ThreadSafty.java(同步锁的实现实例、线程同步的两个案例:账户操作例程、分苹果例程)
package blog5; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 线程同步安全问题,本章重点,本例代码实际是融合了3个习题 * 通过实例阐述三种实现线程同步的方法: * 1 信用卡账户问题(同步代码块):建立一个信用卡账户,起始信用额100w,然后模拟透支、存款等 * 多个操作,显然银行账户User对象是个竞争资源,应该把修改余额 * 的语句放在同步代码块中,将余额设为私有变量防止直接访问。 * 2 小朋友分苹果问题(同步方法):小朋友小明和小强分别取拿苹果,则苹果数的修改应放在同步方法中。 * 实现模拟两个小朋友拿苹果的过程。 * 3 同步锁Lock的实现:模拟多个线程卖票过程 */ public class ThreadSafty { /* 练习1 // 测试信用卡庄户问题:同步代码块 public static void main(String[] args) { //建立一个用户,和六个操作该用户的线程。 User u = new User("张三",100); //注:此处User u 以参数形式传递给六个线程,那么6个线程中的属性User是同一个u,还是分别根据参数 //在内存中新建6个相同的u,我觉得是引用数据类型传递的是栈的地址,也就是说所有线程面对的是同一个u BankCountThread bkt1 = new BankCountThread("线程1", u, 30); BankCountThread bkt2 = new BankCountThread("线程2", u, -30); BankCountThread bkt3 = new BankCountThread("线程3", u, -60); BankCountThread bkt4 = new BankCountThread("线程4", u, 20); BankCountThread bkt5 = new BankCountThread("线程5", u, 100); BankCountThread bkt6 = new BankCountThread("线程6", u, -90); bkt1.start(); bkt2.start(); bkt3.start(); bkt4.start(); bkt5.start(); bkt6.start(); } */ /* 练习2 //测试分苹果问题:同步方法 public static void main(String[] args) { AppleThread apple = new AppleThread(); Thread thread1 = new Thread(apple); Thread thread2 = new Thread(apple); thread1.setName("明明"); thread2.setName("强强"); thread1.start(); thread2.start(); } */ // 练习3 // 同步锁lock实现:买票过程 public static void main(String[] args) { SaleTicketsThread saleThread = new SaleTicketsThread(); Thread t1 = new Thread(saleThread); Thread t2 = new Thread(saleThread); Thread t3 = new Thread(saleThread); t1.setName("线程A"); t2.setName("线程B"); t3.setName("线程C"); t1.start(); t2.start(); t3.start(); } } //练习1,银行账户线程,采用继承Thread方法 class BankCountThread extends Thread{ private User u; private int y=0; public BankCountThread(String name,User u,int y) { super(name); this.u = u; this.y = y; } @Override public void run() { u.oper(y); } } //练习1辅助类:银行账户类 class User{ @SuppressWarnings("unused") private String code; //id code number private int cash; //balance in the count public User(String code,int cash) { this.cash = cash; this.code = code; } //存取操作 public void oper(int x){ try{ Thread.sleep(10); synchronized(this){ this.cash += x; System.out.println(Thread.currentThread().getName() +"操作,增加了"+ x +",当前余额为:"+this.cash); } Thread.sleep(10); }catch ( InterruptedException e){ e.printStackTrace(); } } } //练习2:分苹果线程,采用实现Runnable接口方法 class AppleThread implements Runnable{ private int appleCount = 5; //拿苹果方法,synchronized方法实际是以此对象实例this为锁的,保证一次只有一个线程进入修改appleCount public synchronized boolean getApple(){ if(appleCount>0){ //the apple count more than one,take an apple appleCount--; try { //拿苹果模拟需要1分钟,也充分可以测试到线程安全问题 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"拿走了一个苹果,还剩下个" + appleCount + "苹果"); return true; }else{ return false; } } @SuppressWarnings("unused") public void run() { boolean flag; while( (flag = getApple()) ){ //循环执行getApple,直到getApple返回false,也就是苹果被拿完 } System.out.println(Thread.currentThread().getName()+"线程结束..."); } } // 练习3:同步锁lock实现线程安全编程 class SaleTicketsThread implements Runnable { private int tickets = 50; //票总数 private Lock myLock = new ReentrantLock();//同步锁对象 @SuppressWarnings("unused") @Override public void run() { boolean flag; while( (flag = sale()) ){ } System.out.println(Thread.currentThread().getName() + "结束操作..."); } // 卖票操作 public boolean sale(){ myLock.lock(); //上锁 if(tickets>0){ try { tickets--; System.out.println(Thread.currentThread().getName() + "卖出1张票,还剩 " + tickets + " 张票..."); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } finally{ myLock.unlock(); //解锁 } return true; }else{ myLock.unlock(); //解锁,不要忘了这一句,否则会有线程永远无法结束,永远处于等待锁的状态 return false; } } } //用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而用Lock需要我们手动释放锁, //所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内。 //如果说这就是Lock,那么它不能成为同步问题更完美的处理方式,通常还有一种更好用的读写锁(ReadWriteLock), //我们会有一种需求,在对数据进行读写的时候,为了保证数据的一致性和完整性,需要读和写是互斥的,写和写是互斥的, //但是读和读是不需要互斥的,这样读和读不互斥性能更高些。
6、线程通信
典型的生产者与消费者问题:生产者和消费者共享存放产品的仓库,若仓库为空时,消费者无法消费,仓满时,生产者无法继续生产。Java通过wait、notify、notifyAll三个方法提供了线程的通信。这三个方法都只能在synchronized关键字范围内使用。
wait()方法:中断方法的执行,使本线程等待,暂时让出 cpu 的使用权,并允许其他线程使用这个同步方法,即同时让出对象锁,直到其他使用同一个对象锁线程调用notify方法或notifyAll方法为止。
notify()方法:唤醒由于使用这个同步方法(同步对象锁)而处于等待线程的 某一个结束等待。
notifyall()方法:唤醒所有由于使用这个同步方法(同步对象锁)而处于等待的线程结束等待。
以上三个方法是Object的方法而不是Thread的成员方法,这三个方法必须用同步对象来调用。若是synchronized同步方法,则因为该类默认的实例this就是同步对象,可以在同步方法中调用这三个方法;若是synchronized修饰的同步代码块,同步锁是括号中的对象,所以必须使用该对象调用对象的这三个方法。
若用Lock代替同步方法或同步代码块,Condition代替同步对象锁,Condition对象通过Lock对象的newCondition方法创建,await等价wait,signal等价notify,signalAll等价notifyAll。
Wait和sleep的区别:1、这两个方法来自不同的类分别是Thread和Object;2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法;3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围);4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
ThreadCommunite.java(两个实例介绍线程通信:买票实例、生产消费者实例,一个lock线程通信实例)
package blog5; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 线程通信的实例,本章重点,同样本文件融合三个练习。 * 通过3个实例介绍本知识点 * 1 生产者与消费者实例 * 2 买票实例:例题:A,B,C,买票,5元一张,售票员目前只有一张5元零钱 A持20元,B、C持5元去排队买票,若A排在前面,只能先等待,B、C买完之后再买(此时才能找零) * 3 lock实现的线程通信:此处摘抄一段他人总结的代码作为学习 */ public class ThreadCommnunite { /* 习题1 // 生产者与消费者:生产者不断的生成A—Z的数据,而消费者不断取出数据,但是必须生成之后再取 public static void main(String[] args) { //创建数据对象和消费者、生产者线程 ShareDate date = new ShareDate(); new Thread(new Productor(date)).start(); new Thread(new Consumer(date)).start(); } */ // 习题2 // 买票例程:A,B,C,买票,5元一张,售票员目前只有一张5元零钱 // A持20元,B、C持5元去排队买票,若A排在前面,只能先等待,B、C买完之后再买(此时才能找零) public static void main(String[] args) { Saler saleThread = new Saler(); Thread t1 = new Thread(saleThread); Thread t2 = new Thread(saleThread); Thread t3 = new Thread(saleThread); t1.setName("A"); t2.setName("B"); t3.setName("C"); t1.start(); t2.start(); t3.start(); } } //习题1:生产者与消费者共享数据 class ShareDate{ private char date; private boolean writeable = true; //可以写入吗?初始为可写入,程序开始先写再取 //写数据 public synchronized void setDate(char d){ if(!writeable){ try { //此时不能写入,writeable == false,即写了还没取出,未消费,此时等待 wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //此时可以写入,writeable == true,则生产一个数据 this.date = d; writeable = false; //修改状态标志,变成不能写,需要消费。 notify(); //唤醒等待中的线程 } //此时两个方法 getDate和setDate都是synchronized是否使用同一个锁对象this?我想是的......... public synchronized char getDate(){ if(writeable){ try { //此时标识告诉我们应该写入,所有等待写如数据,等待生产 wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //此时writeable==false,即生产已经结束,可以消费 writeable = true; notify(); return date; } } //练习1:生产者类 class Productor implements Runnable{ private ShareDate shareDate; public Productor(ShareDate shareDate) { this.shareDate = shareDate; } @Override public void run() { for(char c = 'A';c<='Z';c++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } shareDate.setDate(c); System.out.println("生产者生产一个数据:" + c ); } System.out.println("生产任务完成,生产者线程结束..."); } } //练习1:消费者类 class Consumer implements Runnable{ private ShareDate shareDate; public Consumer(ShareDate shareDate) { this.shareDate = shareDate; } @Override public void run() { char c; do{ c = shareDate.getDate(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("..消费者取出一个数据:" + c ); }while(c!='Z'); System.out.println("消费任务完成,消费者线程结束..."); } } // 习题2:售票员类,是一个共享数据,三个买票者线程都是通过该类获得票 class Saler implements Runnable{ private int fiveCount = 1; @Override public void run() { sale(); } //同步方法,一次只能一个人来卖票 public synchronized void sale(){ if("A".equals(Thread.currentThread().getName())){ //当A来买票时,看钱能找开不 if(fiveCount<3){ try { //找不开钱则等待 System.out.println("找零不够,A等待!"); wait(); //当被唤醒等待的时候,就可以卖给A票了 fiveCount -= 3; System.out.println("卖给A一张票,找零15"); } catch (InterruptedException e) { e.printStackTrace(); } }else{ //能找开钱,直接卖给A,这种情况其实不会发生 fiveCount -= 3; System.out.println("卖给A一张票,找零15"); } }else { //当是B或者C来买票的时候,B或者C进入该方法 fiveCount++; System.out.println("卖给"+ Thread.currentThread().getName()+"一张票"); if(fiveCount>=3){ notify(); //当钱够了叫醒A线程来买票 } } } } // 习题3:lock实现的线程通信 class BoundedBuffer { final Lock lock = new ReentrantLock();//锁对象 final Condition notFull = lock.newCondition();//写线程条件 final Condition notEmpty = lock.newCondition();//读线程条件 final Object[] items = new Object[100];//缓存队列 int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length)//如果队列满了 notFull.await();//阻塞写线程 items[putptr] = x;//赋值 if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0 ++count;//个数++ notEmpty.signal();//唤醒读线程 } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0)//如果队列为空 notEmpty.await();//阻塞读线程 Object x = items[takeptr];//取值 if (++takeptr == items.length) takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0 --count;//个数-- notFull.signal();//唤醒写线程 return x; } finally { lock.unlock(); } } // 这是一个处于多线程工作环境下的缓存区,缓存区提供了两个方法,put和take // put是存数据,take是取数据,内部有个缓存队列,具体变量和方法说明见代码 // 这个缓存区类实现的功能:有多个线程往里面存数据和从里面取数据 // 其缓存队列(先进先出后进后出)能缓存的最大数值是100,多个线程间是互斥的 // 当缓存队列中存储的值达到100时,将写线程阻塞,并唤醒读线程 // 当缓存队列中存储的值为0时,将读线程阻塞,并唤醒写线程 //优点是能明确哪种情况下唤醒那种线程。 }
附重要知识:重点,线程同步安全和线程通信。
package exercise; //两个线程共同打印0—99,解决线程安全问题。线程同步时的安全问题 /** * 线程知识; * 1、sleep、wait、join、yield的作用、特点、用法(通常sleep、yield少在synchronized中使用,而wait、notify在同步块中使用) * 2、解决 线程同步 问题的方法:synchronized * 3、解决 线程通信 问题的方法:wait 、 notify方法。生产者与消费者实例 * * 生产者与消费者实例三要素:共享的数据(生成和消费的对象,提供消费方法和生产方法,且这两种方法互相通过标识wait、notify)、消费者、生产者 */ public class Test07_Thread { public static void main(String[] args) { MyThread a = new MyThread(); Thread t1 = new Thread(a); Thread t2 = new Thread(a); t1.setName("T1"); t2.setName("T2"); t1.start(); t2.start(); } } class MyThread implements Runnable{ private int i=0; @Override public void run() { try { while(!print()){Thread.yield();} System.out.println("over"); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized boolean print() throws InterruptedException{ if(i<=100){ System.out.println(Thread.currentThread().getName() + " " + i); i++; } Thread.sleep(10); if(i<=100){ return false; }else{ return true; } } }
package exercise; //两个线程共同打印0—99,解决线程安全问题。采用线程通信的方法,产生交替打印的效果 public class Test08_Thread2 { public static void main(String[] args) { Data d = new Data(); Thread t1 = new Thread(d); Thread t2 = new Thread(d); t1.setName("A"); t2.setName("B"); t1.start(); t2.start(); } } class Data implements Runnable{ private int i=0; @Override public void run() { print(); } public synchronized void print(){ for(;i<=100;){ if("A".equals(Thread.currentThread().getName())){ if(i%2==0){ System.out.println(Thread.currentThread().getName() + " " + i); i++; notify(); }else{ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }else{ if(i%2==0){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ System.out.println(Thread.currentThread().getName() + " " + i); i++; notify(); } } } } }
效果:前者交替随机打印。后者AB严格依次打印
相关文章推荐
- JavaSE:JAVA VS C#
- java-多线程技术
- Java多线程技术中所有方法的详细解析
- Java -- 多线程技术基础
- JavaSE学习 第十七章 Java网络编程
- Java学习之路_学好基本功JavaSE
- 手工编译、运行JavaSE项目,且此项目需要第三方jar包支持,且文件中有打包。 分类: java 2010-03-27 10:37 1057人阅读 评论(0) 收藏
- Java游戏开发框架LGame-0.2.8版发布(含JavaSE及Android版,已有文档)
- Java多线程技术中所有方法的详细解析
- java-多线程技术
- 对Java多线程技术中所有方法的详细解析
- 对Java多线程技术中所有方法的详细解析
- Java分为三个体系JavaSE,JavaEE,JavaME 它们的区别以及java的各个版本?
- 对Java多线程技术中所有方法的详细解析
- Java -- 多线程技术基础(1) 方法详解
- JavaSE 6.0 学习(内部类----java世界的多面手)
- 对Java多线程技术中所有方法的详细解析
- JavaSE基础小结--2---Java框架简介
- Java 多线程技术探究
- 100个Java经典例子(1-10)初学者的利器高手的宝典JavaSE