多线程机制
1、进程
进程的概念:主流计算机操作系统都支持同时运行多个任务,每个任务通常就是一个程序,每个运行中的程序就是一个进程或者多个进程。
进程的特点:
独立性
- 进程是系统中独立存在的实体。
- 可以拥有自己独立的资源。
- 拥有自己私有的地址空间。
- 在未经允许的情况下,用户进程不能其他进程的地址空间。
动态性
- 进程是一个正在系统中活动的指令集合。
- 与进程不同的是,程序是一个静态的指令集合。
- 进程具有自己的生命周期和各种不同的状态。
- 与进程不同的是,程序没有时间的概念,也就没有生命周期等概念。
并发性
- 多个进程可以在单个处理器上并发运行。
- 并发运行的各个进程之间不会相互影响。
进程测试案例:
package ecut.threads;
public class JvmProcessTest {
//JvmProcess虚拟机进程
public static void main(String[] args) throws InterruptedException {
Thread.sleep( 30000 );//以毫秒为单位
}
//每次运行一个带main 方法的程序的时候就会产生一个虚拟机进程产生,由它去解析字节码文件,然后给我们产生结果
//Java多进程支持非常弱
}
一个 Java 虚拟机 启动后 对应一个 进程 ( 一个运行中的Java 虚拟机实例就是一个进程)。
运行结果如下:
进程测试案例:
package ecut.threads;
public class JvmProcessStateTest {
public static int counter = 100 ;
public static void main(String[] args) {
counter++ ;
System.out.println( counter );
}
}
一个(次) Java 虚拟机启动后,其内部的数据相对于其它进程来说是独立的,当它退出时,所数据都将丢失 ( JvmProcessStateTest )。
运行结果如下:
每次运行结果都是101。
2、并行与并发
并行: 多个 线程 同时 运行在 不同的 CPU 上。
并发: 同一时刻一个 CPU 只能运行一条指令,多个线程指令快速地轮换执行, 在某个时间段内,看上去具有多个线程同时执行的效果。
并发策略的实现方式:共享式 ( 协作式 )、 抢占式 ( 目前主流的调度方式 )。
3、线程
线程的概念:线程( Thread ) 被称作轻量级进程( Lightweight Process ),线程是比进程更小一级的执行单元。
线程的特点:
- 一个进程可以有多个线程,但至少有一个线程(当进程被初始化后,主线程也就被创建了)。
- 线程不能独立存在,必须属于某个进程(线程可以拥有自己的堆栈、自己的程序计数器、自己的局部变量,线程不能再独立拥有系统资源,它将与父进程的其它线程共享该进程内所有的系统资源)。
- 线程可以独立完成某项任务,也可以跟同一进程的其它线程一起完成某项任务。
- 线程是独立运行的,它并不知道同进程内的其它线程的存在。
- 线程的执行是抢占式的。
- 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
4、多线程的优势
同一个进程的各线程间共享内存非常容易
- 进程不能共享内存,因此进程间通信没那么容易。
用多线程实现多任务并发比多进程效率高
- 系统创建进程需要重新分配系统资源,因此效率不高。
- 而创建线程不需要重新分配系统资源,因此效率较高。
Java 语言简化了多线程编程
- Java 语言内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式。
实际应用中,多线程应用非常广泛
- 浏览器可以同时下载多个图片。
- 迅雷可以同时以n 个线程下载一个文件。
- 一个Web 服务器必须能同时响应多个用户的请求。
- JVM 本身则在后台提供了一个超级线程来回收垃圾。
5、程序、进程和线程
程序 : 就是通常说的 应用程序 ,比如 Office World 、Google Chrome 都是程序 [学校]
- 一个程序开启后,可能对应 一个 或 多个 进程。
- 但是 至少需要有一个进程。
- 如果对应多个 进程,则一定有一个主进程。
进程 : [班级]
- 一个正在运行中的 进程 可能 对应 一个 或 多个线程。
- 一个进程 至少 需要有一个 线程。
- 如果一个进程对应多个线程,则一定有一个是主线程。
- 一个 Java 虚拟机 启动后 对应一个 进程 ( JvmProcessTest )。
- 一个(次) Java 虚拟机启动后,其内部的数据相对于其它进程来说是独立的,当它退出时,所数据都将丢失 ( JvmProcessStateTest )。
线程 : 可以理解成是 轻量级 的进程 [座位]
线程的创建和启动
1、用 Java 语言实现一个线程
线程测试案例:
package ecut.threads; public class ThreadTest1 { public static void main(String[] args) { // 获得当前线程的引用 [当前线程名称,当前线程的优先级,当前线程所在线程组的名称] System.out.println( Thread.currentThread() ); // 创建一个 Thread 类的实例 就是 创建一个线程 Thread t = new Thread(); // 通过 Thread 类的 start 方法来启动线程 t.start(); } }
运行结果如下:
Thread[main,5,main]
2、实现自定义线程
- 继承Tread类。
- 实现Runnable接口。
继承Tread类实现自定义线程测试案例:
package ecut.threads;
/**
* 继承 Thread 类来实现自己的 线程,则需要 重写 run 方法
*/
public class FirstThread extends Thread {
public FirstThread(){
super( "first" ) ; // 调用父类 Thread 的 构造方法
}
public FirstThread( String name ){
super( name ) ; // 调用父类 Thread 的 构造方法
}
@Override
public void run() {
for( int i = 0 ; i < 5 ; i++ ){
System.out.print( i );
System.out.print( " : " );
//获得当前正在线程执行的引用,用Thread.currentThread(),不要用this,this返回的是当前的对象
System.out.println( Thread.currentThread() );
}
}
}
package ecut.threads;
/**
* 应该怎样启动一个线程 ( 启动线程 跟 调用 线程实例的 run 方法的区别 )
* 如果要创建并启动一个 自己的线程 ,需要继承 Thread 类并重写 run 方法
*/
public class ThreadTest2 {
public static void main(String[] args) {
Thread t = new FirstThread();
// t.run(); // main 调用 t 所引用的实例的 run 方法,执行者是main这个线程,当前线程是main
t.start();//调用start启动了t这个线程,让t里面的run方法去执行
}
}
java.lang.thread 是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。 Thread 类 实现了 Runnable 接口 中的 run 方法 ,因此,如果要 通过 继承 Thread 类来实现自己的 线程,则需要 重写 run 方法。
部分源码:
public void run() {
if ( target != null ) {
target.run();
}
}
private Runnable target ;
public Thread( Runnable target )
运行结果如下:
0 : Thread[first,5,main]
1 : Thread[first,5,main]
2 : Thread[first,5,main]
3 : Thread[first,5,main]
4 : Thread[first,5,main]
实现Runnable接口测试案例:
package ecut.threads;
/**
* 实现 Runnable 的类 的实例 只能算作可执行对象,不属于线程
*/
public class SecondThread implements Runnable {
@Override
public void run() {
System.out.println( Thread.currentThread() );
}
}
package ecut.threads; /** * 通过 创建一个 Thread 实例 ,来执行 Runnable 实例的 run 方法 */ public class ThreadTest3 { public static void main(String[] args) { // 创建一个 可运行的对象 ( 它不是线程 ) Runnable runnable = new SecondThread(); // runnable.run();//main线程调 Runnable 所引用的实例的 run 方法,执行者是main这个线程,当前线程是main // 对于 Runnable 类型的 实例,需要用 另外的一个 Thread 来启动它 Thread t = new Thread( runnable , "second"); t.start(); // 启动 线程 t ,将调用 t 内部的 run 方法,而 t 内部的 run 将调用 runnable 的 run 方法 } }
java.lang.Runnable 接口应该由那些打算通过 某一线程 执行 其实例的类来实现。使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法。public abstract void run() ; Runnable 接口中的 run 方法 用来指定 线程需要 做什么。如果要让 Runnable 类型的实例 运行起来,需要通过另外一个 线程来启动。
运行结果如下:
Thread[second,5,main]
线程的状态
1、新建状态
- 使用new 关键字创建的线程对象,处于新建状态。
- 注意这里说说的使用new 关键字是本质上使用new关键字很可能调用某个方法获得一个线程,只要这个方法中使用了new 关键字,那么这个线程就是新建状态。
- 比如Thread t = new Thread(); 语句就新建了一个线程对象。
2、就绪状态
- 一个线程被创建后,并不是立即运行。
- 其他线程调用该线程的start() 方法,会使该线程处于就绪状态。
- 这时,JVM就会为它创建方法调用栈和PC寄存器,该状态的线程处于可运行池中,等待获得CPU 的使用权。
3、运行状态
- 该状态的线程占用CPU ,执行其程序代码。
- 只有处于就绪状态的线程才能有机会转到该状态。
- 单一CPU ,任一时刻只会有一个线程处于该状态。
- 多CPU环境下,会有多个线程同时处于该状态。
4、阻塞状态
- 指线程因为放弃CPU,暂停运行。
- 线程处于阻塞状态,JVM不会给线程分配CPU。
- 阻塞状态分3中情况:位于对象等待池中的阻塞状态(处于运行状态的线程,某个对象调用了wait() 方法)、处于对象锁池中的阻塞状态(处于运行状态的线程,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,JVM会把这个线程放到这个对象的锁池中。)、其他阻塞状态(线程执行了sleep() 方法、或者调用了其他线程的join() 方法、或者发出了I/O 请求,就会进入该状态)。
5、死亡状态
- 当线程退出run() 方法,线程就处于死亡状态。
- 该线程的生命周期也就结束了。
- 线程退出run() 方法,有可能是程序执行完毕,也有可能是程序异常中断,无论是正常退出还是异常中断,都不会对其他线程造成影响。
线程调度
1、线程调度的概念
- 单个CPU,任意时刻只能执行一条机器指令。
- 每个线程只有获得CPU的使用权才能执行指令。
- 线程调度就是按照特定的机制给多个线程分配CPU使用权。
2、调度模式
- 分时调度(协作)模式:让所有线程都轮流获得CPU的使用权,并且平均分配每个线程所占用的CPU时间片。
- 抢占式调度模式:优先让线程池中优先级高的线程占用CPU,当优先级比较高的线程执行完毕后优先级较低的线程才有机会执行,优先级相同的多个线程,谁先抢到时间片谁就运行
3、实现调度的方式
- 调整各个线程的优先级:优先级高的线程获得较多的运行机会,优先级低的线程获得较少的运行机会。
- 让处于运行状态的线程调用Thread.sleep() 方法:处于运行状态的线程会转入睡眠状态,其它线程会获得CPU时间片。
- 让处于运行状态的线程调用Thread.yield() 方法:处于运行状态的线程让位给同等优先级或优先级更高的线程运行
- 让处于运行状态的线程调用另一个线程的join() 方法:当前线程A调用另一个线程B的join() 方法,当前线程A转入阻塞状态,直到另一个线程B执行结束,线程A才恢复运行
线程优先级测试案例:
package ecut.threads; public class PriorityThread implements Runnable { @Override public void run() { // 获得 当前线程 的引用 Thread current = Thread.currentThread() ; for( int i = 0 ; i < 5 ; i++ ){ System.out.println( i + " : " + current.getName() + " , " + current.getPriority() ); } } }
package ecut.threads; public class PriorityThreadTest { public static void main(String[] args) { // 创建一个 可以运行 的实例 Runnable runnable = new PriorityThread(); // 创建一个专门用来 执行 runnable 的 线程 Thread t1 = new Thread( runnable , "first" ); t1.setPriority( Thread.NORM_PRIORITY - 2 ); t1.start(); Thread t2 = new Thread( runnable , "second" ); t2.setPriority( Thread.NORM_PRIORITY + 2 ); t2.start(); Thread t3 = new Thread( runnable , "third" ); t3.start(); } }
优先级越高的线程 获得 CPU 时间片 的机会越高,调整线程的优先级,可以使用 Thread .setPriority( int priority )(非静态方法),获得线程的优先级,可以使用 Thread . getPriority()(非静态方法),与优先级有关的常量: Thread.NORM_PRIORITY, Thread.MIN_PRIORITY, Thread.MAX_PRIORITY。
运行结果如下:
0 : first , 3
0 : second , 7
1 : second , 7
0 : third , 5
1 : first , 3
1 : third , 5
2 : third , 5
3 : third , 5
4 : third , 5
2 : second , 7
3 : second , 7
2 : first , 3
4 : second , 7
3 : first , 3
4 : first , 3
线程睡眠测试案例:
package ecut.threads; public class SleepThread implements Runnable { @Override public void run() { // 获得 当前线程 的引用 Thread current = Thread.currentThread() ; for( int i = 0 ; i < 5 ; i++ ){ System.out.println( i + " : " + current.getName() + " , " + current.getPriority() ); try{ Thread.sleep( 1 ); // 睡眠的时间单位是毫秒,1毫秒足够有时间让别的线程抢占cpu } catch ( InterruptedException e ) { //InterruptedException不能抛出,因为父类没有抛出异常,每次try catch 都会创建一个异常追踪的堆栈,消耗内存和空间效率低。 System.out.println( "居然敢打扰老子睡觉..." ); } } } }
package ecut.threads; public class SleepThreadTest { public static void main(String[] args) { // 创建一个 可以运行 的实例 Runnable runnable = new SleepThread(); // 创建一个专门用来 执行 runnable 的 线程 Thread t1 = new Thread( runnable , "first" ); t1.setPriority( Thread.MIN_PRIORITY ); t1.start(); Thread t2 = new Thread( runnable , "second" ); t2.start(); Thread t3 = new Thread( runnable , "third" ); t3.setPriority( Thread.MAX_PRIORITY ); t3.start(); } }
Thread.sleep( long millis )(静态方法)在任意的线程中 调用 Thread.sleep 方法可以让当前正在执行的线程进入"睡眠" 状态 ( 阻塞状态 ),Thread.sleep() 导致 正在执行的 当前线程 ( Thread.currentThread() ) 进入睡眠状态, 从而释放 CPU 让其它的线程 有机会 可以获得 CPU ( 并执行其线程体中的代码 ),这与 线程的 优先级无关,无论是 优先级高的线程、优先级低的线程 都有机会获得 CPU。sleep() 给其他线程运行的机会,不考虑其他线程的优先级。
运行结果如下:
0 : first , 1 0 : second , 5 0 : third , 10 1 : third , 10 1 : second , 5 1 : first , 1 2 : second , 5 2 : first , 1 2 : third , 10 3 : second , 5 3 : first , 1 3 : third , 10 4 : second , 5 4 : third , 10 4 : first , 1
睡眠中断测试案例:
package ecut.threads; public class InterruptedSleepThread implements Runnable{ public static void main(String[] args) { Runnable runnable = new InterruptedSleepThread() ; Thread t = new Thread( runnable , "interrupted" ); t.start(); t.interrupt(); // 中断 阻塞状态 ( 可能是中断睡眠 ) } @Override public void run() { try { Thread.sleep( 3000 ); System.out.println( "好好睡觉,天天向上" ); } catch (InterruptedException e) { System.err.println( "居然敢打扰老子睡觉..." ); e.printStackTrace(); } } }
运行结果如下:
居然敢打扰老子睡觉... java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at ecut.threads.InterruptedSleepThread.run(InterruptedSleepThread.java:21) at java.lang.Thread.run(Thread.java:745)
线程等待测试案例:
package ecut.threads; public class JoinThread implements Runnable { private Thread another ; public JoinThread() { super(); } public JoinThread(Thread another) { super(); this.another = another; } @Override public void run() { // 获得 当前线程 的引用 Thread current = Thread.currentThread() ; for( int i = 0 ; i < 6 ; i++ ){ System.out.println( i + " : " + current.getName() + " , " + current.getPriority() ); if( i == 2 ){ if( another != null ) { try { // 调用另外一个线程的 join 方法,让 那个线程 的 代码 执行完之后,当前线程才继续执行 another.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
package ecut.threads; public class JoinThreadTest { public static void main(String[] args) { Runnable runnable1 = new JoinThread(); // 因为不知道要调用谁的 join 方法,所以不传参数 Thread t1 = new Thread( runnable1 , "first" ); t1.setPriority( Thread.MIN_PRIORITY ); Runnable runnable2 = new JoinThread( t1 ); // 传入需要等待的线程 ( Thread ) Thread t2 = new Thread( runnable2 , "second" ); t2.start(); t1.start(); } }
Thread .join() (非静态方法)A 线程 内部 调用了 B 线程的 join 方法,将导致 A 线程进入等待状态 ( 阻塞状态 ),等到 B 线程执行结束后,A 线程在继续执行, join不考虑线程的优先级。
运行结果如下:
0 : second , 5 1 : second , 5 0 : first , 1 1 : first , 1 2 : second , 5 2 : first , 1 3 : first , 1 4 : first , 1 5 : first , 1 3 : second , 5 4 : second , 5 5 : second , 5
线程让位测试案例:
package ecut.threads; public class YeildThread implements Runnable { @Override public void run() { // 获得 当前线程 的引用 Thread current = Thread.currentThread() ; for( int i = 0 ; i < 5 ; i++ ){ System.out.println( i + " : " + current.getName() + " , " + current.getPriority() ); Thread.yield(); // 当前线程将让位,进入到 就绪状态 } } }
package ecut.threads; public class YeildThreadTest { public static void main(String[] args) { Runnable runnable = new YeildThread(); Thread t1 = new Thread( runnable , "first" ); t1.setPriority( Thread.MIN_PRIORITY ); t1.start(); Thread t2 = new Thread( runnable , "second" ); t2.setPriority( Thread.MIN_PRIORITY + 2 ); t2.start(); Thread t3 = new Thread( runnable , "third" ); t3.setPriority( Thread.NORM_PRIORITY ); t3.start(); Thread t4 = new Thread( runnable , "forth" ); t4.setPriority( Thread.MAX_PRIORITY ); t4.start(); } }
Thread.yeild()(静态方法)当一个正在运行的线程调用了 Thread.yeild() 后, 当前正在运行的线程将释放 CPU , 以便于 让 跟 自己具有 同等优先级 或 比 自己优先级高的线程 有机会 获得 CPU , 并且,它本身 还将参与 下次竞争。
运行结果如下:
0 : second , 3 1 : second , 3 0 : third , 5 1 : third , 5 0 : forth , 10 1 : forth , 10 0 : first , 1 1 : first , 1 2 : forth , 10 2 : third , 5 3 : third , 5 4 : third , 5 2 : second , 3 3 : forth , 10 4 : forth , 10 2 : first , 1 3 : second , 3 4 : second , 3 3 : first , 1 4 : first , 1
精灵线程测试案例:
package ecut.threads; public class YeildThread implements Runnable { @Override public void run() { // 获得 当前线程 的引用 Thread current = Thread.currentThread() ; for( int i = 0 ; i < 5 ; i++ ){ System.out.println( i + " : " + current.getName() + " , " + current.getPriority() ); Thread.yield(); // 当前线程将让位,进入到 就绪状态 } } }
package ecut.threads; public class YeildThreadTest { public static void main(String[] args) { Runnable runnable = new YeildThread(); Thread t1 = new Thread( runnable , "first" ); t1.setPriority( Thread.MIN_PRIORITY ); t1.start(); Thread t2 = new Thread( runnable , "second" ); t2.setPriority( Thread.MIN_PRIORITY + 2 ); t2.start(); Thread t3 = new Thread( runnable , "third" ); t3.setPriority( Thread.NORM_PRIORITY ); t3.start(); Thread t4 = new Thread( runnable , "forth" ); t4.setPriority( Thread.MAX_PRIORITY ); t4.start(); } }
Thread.setDaemon( boolean on )(非静态方法)将该线程标记为守护线程或用户线程。通过 Thread t = new Thread( runnable ) ;创建一个新的线程在启动线程之前就应该将 某个 线程 设置为 精灵线程 t.setDaemon( true ) ;之后再启动线程 t.start();yeild考虑优先级。
运行结果如下:
一直输出hello直到线程结束。
hello
hello
........
精灵线程的特点是:
- 随着 前台线程 的存在而存在,在后台执行
- 当所有 前台线程都退出后,则 所有的 精灵线程 也可以结束了,此时 JVM 进程也将结束
- 比较著名的精灵线程是 垃圾回收线程
线程同步
1、线程安全
线程安全概念:多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的。
线程安全测试案例:
package ecut.threads.synchronize; public class Account { private String id ; /* 当前账户的编号 */ private double balance ; /* 账户余额 */ public Account( String id, double balance ) { super(); if( id == null || id.trim().isEmpty() ){ // 如果 id 为 null 或者 是 空串 ( 剔除首尾空白 ) throw new RuntimeException( "账号不能为空" ); } this.id = id; if( balance < 0 ){ throw new RuntimeException( "新开的账户余额怎么能是负数?" ); } this.balance = balance; } public String getId() { return id; } public void setId(String id) { this.id = id; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } }
package ecut.threads.synchronize; public class DrawThread implements Runnable { private Account account; private double money; public DrawThread(Account account, double money) { super(); if (account == null) { // 如果 account 为 null throw new RuntimeException("账号不能为空"); } this.account = account; if (money <= 0) { throw new RuntimeException("交易金额必须大于 0"); } this.money = money; } @Override public void run() { Thread current = Thread.currentThread(); String name = current.getName(); System.out.println(name + " : 取款之前,您账户上的余额是: [ " + account.getBalance() + " ]"); // 判断 账户上的 余额 是否 大于 交易金额 if (account.getBalance() > money) { System.out.println(name + " : 您本次的取款金额是 : [ " + money + " ]"); System.out.println(name + " : 请收好您的钞票: " + money); Thread.yield(); double newBalance = account.getBalance() - money; // 让原来的 余额 减去 // 本次交易金额 account.setBalance(newBalance); // 将交易后的金额 重新设置到 account 的 balance // 属性上 System.out.println(name + " : 取款之后,您账户上的余额是: [ " + account.getBalance() + " ]"); } else { System.out.println(name + " : 余额不足,无法交易"); } } }
package ecut.threads.synchronize; public class CCBATM { public static void main(String[] args) { Account a = new Account( "郭靖" , 1000 ); Runnable target1 = new DrawThread( a , 800 ); Thread t1 = new Thread( target1 , "郭靖" ) ; t1.start(); Runnable target2 = new DrawThread( a , 800 ); Thread t2 = new Thread( target2 , "黄蓉" ) ; t2.start(); } }
线程安全其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。
运行结果如下:
郭靖 : 取款之前,您账户上的余额是: [ 1000.0 ] 黄蓉 : 取款之前,您账户上的余额是: [ 1000.0 ] 黄蓉 : 您本次的取款金额是 : [ 800.0 ] 黄蓉 : 请收好您的钞票: 800.0 郭靖 : 您本次的取款金额是 : [ 800.0 ] 黄蓉 : 取款之后,您账户上的余额是: [ 200.0 ] 郭靖 : 请收好您的钞票: 800.0 郭靖 : 取款之后,您账户上的余额是: [ -600.0 ]
2、线程同步
线程同步概念:将操作共享数据的代码行作为一个整体,同一时间只允许一个线程执行,执行过程中其他线程不能参与执行。目的是为了防止多个线程访问一个数据对象时,对数据造成的破坏。Java的多线程支持中引入了同步锁来实现线程同步以达到解决线程安全的问题目的。
3、实现线程同步的两种方式
- 同步代码块。
- 同步方法。
同步代码块测试案例:
package ecut.threads.synchronize; public class DrawThread implements Runnable { private Account account ; private double money ; public DrawThread( Account account , double money ) { super(); if( account == null ){ // 如果 account 为 null throw new RuntimeException( "账号不能为空" ); } this.account = account; if( money <= 0 ){ throw new RuntimeException( "交易金额必须大于 0" ); } this.money = money ; } @Override public void run() { Thread current = Thread.currentThread(); String name = current.getName() ; synchronized ( account ) { System.out.println( name + " : 取款之前,您账户上的余额是: [ " + account.getBalance() + " ]" ); // 判断 账户上的 余额 是否 大于 交易金额 if( account.getBalance() > money ){ System.out.println( name + " : 您本次的取款金额是 : [ " + money + " ]" ); System.out.println( name + " : 请收好您的钞票: " + money ); Thread.yield(); double newBalance = account.getBalance() - money ; // 让原来的 余额 减去 本次交易金额 account.setBalance( newBalance ); // 将交易后的金额 重新设置到 account 的 balance 属性上 System.out.println( name + " : 取款之后,您账户上的余额是: [ " + account.getBalance() + " ]" ); } else { System.out.println( name + " : 余额不足,无法交易" ); } } // 同步代码块 } }
同步代码块的执行原理:获得同步锁 ---> 执行同步代码块 ---> 释放同步锁,所谓 同步锁 就是 几个线程 竞争的资源 ( 竞争什么资源,就拿什么 当 同步锁 ),如果 在 同步代码块中 调用了 Thread.sleep 方法,那么 当前线程 会 带着 同步锁 去睡眠。
synchronized ( 同步锁 ) { // 这里的代码就是 同步代码块 ( 获得 同步锁的 线程才可以执行 同步代码块 ) }
运行结果如下:
郭靖 : 取款之前,您账户上的余额是: [ 1000.0 ] 郭靖 : 您本次的取款金额是 : [ 800.0 ] 郭靖 : 请收好您的钞票: 800.0 郭靖 : 取款之后,您账户上的余额是: [ 200.0 ] 黄蓉 : 取款之前,您账户上的余额是: [ 200.0 ] 黄蓉 : 余额不足,无法交易
对于以上结果,无论单核CPU、双核CPU,结果一样。
同步方法测试案例:
package ecut.threads.synchronize; public class IcbcAccount { private String id; /* 当前账户的编号 */ private double balance; /* 账户余额 */ public IcbcAccount(String id, double balance) { super(); this.id = id; this.balance = balance; } public synchronized void draw( double money ) { Thread current = Thread.currentThread(); String name = current.getName() ; System.out.println( name + " : 取款之前,您账户上的余额是: [ " + balance + " ]" ); // 判断 账户上的 余额 是否 大于 交易金额 if( balance > money ){ System.out.println( name + " : 您本次的取款金额是 : [ " + money + " ]" ); System.out.println( name + " : 请收好您的钞票: " + money ); Thread.yield(); double newBalance = balance - money ; // 让原来的 余额 减去 本次交易金额 this.balance = newBalance ; // 将交易后的金额 重新设置到 account 的 balance 属性上 System.out.println( name + " : 取款之后,您账户上的余额是: [ " + balance + " ]" ); } else { System.out.println( name + " : 余额不足,无法交易" ); } } public String getId() { return id; } public void setId(String id) { this.id = id; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } }
package ecut.threads.synchronize; public class IcbcDrawThread implements Runnable { private IcbcAccount account ; private double money ; public IcbcDrawThread( IcbcAccount account , double money ) { super(); if( account == null ){ // 如果 account 为 null throw new RuntimeException( "账号不能为空" ); } this.account = account; if( money <= 0 ){ throw new RuntimeException( "交易金额必须大于 0" ); } this.money = money ; } @Override public void run() { // 调用同步方法 ,对于 同步方法 draw 来说 当前的对象 ( account ) 就是 同步锁 account.draw( money ); } }
package ecut.threads.synchronize; public class IcbcATM { public static void main(String[] args) { IcbcAccount ia = new IcbcAccount( "杨过" , 1000 ) ; Runnable run1 = new IcbcDrawThread( ia , 800 ); Thread t1 = new Thread( run1 , "杨过" ); Runnable run2 = new IcbcDrawThread( ia , 800 ); Thread t2 = new Thread( run2 , "龙姑娘" ); t1.start(); t2.start(); } }
如果某个方法 被 synchronized 修饰,则该方法即为 同步方法。所有的 同步方法 的 同步锁 都是 当前对象 ( this ) ,比如 :account.draw( money ) ;此时的 同步锁是 account。
public class Account { public synchronized void draw( double money ){ } }
运行结果如下:
杨过 : 取款之前,您账户上的余额是: [ 1000.0 ] 杨过 : 您本次的取款金额是 : [ 800.0 ] 杨过 : 请收好您的钞票: 800.0 杨过 : 取款之后,您账户上的余额是: [ 200.0 ] 龙姑娘 : 取款之前,您账户上的余额是: [ 200.0 ] 龙姑娘 : 余额不足,无法交易
线程通信
1、线程通信的涵义
多线程通信
- 通过访问共享变量的方式(注:需要处理同步问题)
a) 通过内部类实现线程的共享变量
b) 通过实现Runnable 接口实现线程的共享变量
- 通过管道流
PipedInputStream / PipedOutputStream
PipedReader / PipedWriter
Piped.SinkChannel / Piped.SourceChannel ( 属于nio 范围)
多个线程协调运行(依赖于Object的5个方法)
- wait() : 导致当前线程等待
- notify() : 唤醒在此同步锁上等待的单个线程(选择具有任意性)
- notifyAll() : 唤醒在此同步锁上等待的所有线程
2、多线程通信
数据传递的概念:一个线程中的数据 ,传递给另外一个线程。
数据传递的方式:
- 共享变量。
- 使用管道流。
共享变量实现数据传递测试案例测试案例:
package ecut.threads; /** * 模拟下载过程的 线程 */ public class ThunderDownloadThread implements Runnable { private Thunder thunder ; public ThunderDownloadThread(Thunder thunder) { super(); if( thunder == null ){ throw new NullPointerException( "必须指定一个 Thunder 实例 ( 对象 )" ); } this.thunder = thunder; } @Override public void run() { for( int i = 0 ; i < 1024 ; i++ ){ System.out.println( "正在下载:" + i ); } thunder.finish = true ; } }
package ecut.threads; /*** * 模拟保存数据到本次磁盘的线程 */ public class ThunderStoreThread implements Runnable { private Thunder thunder ; public ThunderStoreThread(Thunder thunder) { super(); if( thunder == null ){ throw new NullPointerException( "必须指定一个 Thunder 实例 ( 对象 )" ); } this.thunder = thunder; } @Override public void run() { while( true ){ if( thunder.finish ) { System.out.println( "保存到磁盘" ); break ; } } } }
package ecut.threads; public class Thunder { public static void main(String[] args) { Thunder t = new Thunder(); // t.finish --> false Runnable down = new ThunderDownloadThread( t ); Thread dt = new Thread( down ) ; dt.start(); Runnable store = new ThunderStoreThread( t ); Thread st = new Thread( store ) ; st.start(); } // 声明一个属性,用来表示 "下载" 是否完成 protected volatile boolean finish = false ; }
volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。一个 volatile 对象引用可能是 null。
运行结果如下:
finish字段没有volatile修饰的时候控制台没有打印"保存到磁盘",反之控制台打印了"保存到磁盘"。
使用管道实现数据传递测试案例:
package ecut.threads; import java.io.IOException; import java.io.PipedWriter; public class PipedWriterThread implements Runnable { private PipedWriter writer ; public PipedWriterThread(PipedWriter writer) { super(); this.writer = writer; } @Override public void run() { try { writer.write( "hello , i am PipedWriterThread ." ); } catch (IOException e) { e.printStackTrace(); } } }
package ecut.threads; import java.io.IOException; import java.io.PipedReader; public class PipedReaderThread implements Runnable { private PipedReader reader ; public PipedReaderThread(PipedReader reader) { super(); this.reader = reader; } @Override public void run() { System.out.println( "读取到的数据:" ); try { int ch ; while( ( ch = reader.read() ) != -1 ){ char c = (char)ch ; System.out.print( c ); } } catch (IOException e) { String message = e.getMessage(); if( "write end dead" .equalsIgnoreCase( message ) ) { System.out.println(); System.out.println( "发送数据的线程结束" ); } else { e.printStackTrace(); } } } }
package ecut.threads; import java.io.IOException; import java.io.PipedReader; import java.io.PipedWriter; public class PipedTest { public static void main(String[] args) throws IOException { PipedWriter writer = new PipedWriter(); PipedReader reader = new PipedReader(); writer.connect( reader );//连接两个流 // reader.connect( writer ); Runnable w = new PipedWriterThread(writer); Runnable r = new PipedReaderThread(reader); Thread wt = new Thread( w ); wt.start(); Thread rt = new Thread( r ); rt.start(); } }
运行结果如下:
读取到的数据:
hello , i am PipedWriterThread .
发送数据的线程结束
3、多个线程协调运行
多线程协调运行的实现:
- 必须借助Object 类的wait() 、notify()、notifyAll() 方法实现。
- 这三个方法,必须是基于同步锁的调用,而不是基于线程的调用。
- 对于synchronized 修饰的方法,因为该类的实例(this)即为同步锁。
- synchronized 修饰的同步代码块,其同步锁是synchronized 后面的括号里面的对象。
三个方法的说明:
- wait()方法
导致当前线程等待(进入等待池),直至其它线程调用该同步锁的notify() 方法或者notifyAll() 方法将其唤醒。
调用该同步锁的wait() 方法的当前线程会释放对该同步锁的锁定。
wait() 方法有三种重载形式:
wait() : 一直等待,直到有其它线程通知唤醒。
wait(long timeout) : 在timeout 毫秒后自动苏醒。
wait(long timeout, int nanos) : 在timeout 毫秒和nanos 微妙后自动苏醒。
- notify()方法
唤醒在此同步锁上等待的单个线程。
如果所有的线程都在此同步锁上等待,则会任意唤醒其中的一个线程。
只有当前线程放弃对该同步锁的锁定后(使用wait()方法),才可以执行被唤醒的线程。
- notifyAll()方法
唤醒正在等待该同步锁的所有线程。
只有当前线程放弃对该同步锁的锁定后,才可以执行被唤醒的线程。
多个线程协调测试案例:
package ecut.threads; /** * BOC : Bank of China */ public class BocAccount { /* 定义一个枚举,用来表示 账户上有钱还是没钱 */ public static enum Mark { RICH("有钱") , NONE("没钱") ; private String value; private Mark(String value) { this.value = value; } public String getValue() { return value; } } private String id; /* 当前账户的编号 */ private double balance; /* 账户余额 */ private Mark mark = Mark.NONE; // 代表是否有钱的标记: 有- 帐号上有钱, 无- 帐号上没有钱 public BocAccount(String id, double balance) { super(); this.id = id; if( balance > 0 ) { this.balance = balance ; mark = Mark.RICH ; } } public String getId() { return id; } public void setId(String id) { this.id = id; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } public Mark getMark() { return mark; } public void setMark(Mark mark) { this.mark = mark; } }
package ecut.threads; /** * 表示学生 */ public class Student { private BocAccount account; public Student(BocAccount account) { super(); this.account = account; } public void draw(double drawAmount) { synchronized ( account ) { try { if (account.getMark() == BocAccount.Mark.NONE ) { System.out.println( Thread.currentThread().getName() + " : 卡上没钱,白跑一趟" ); // this.wait(); // 注意,这里的 this 指的是 当前的 Student 对象 ( 此时它并不是同步锁 ) account.wait(); // 谁是同步锁,就调用谁的 wait 方法 } else { System.out.println(Thread.currentThread().getName() + " - 取钱[" + drawAmount + "]"); account.setBalance( account.getBalance() - drawAmount ); System.out.println(Thread.currentThread().getName() + " - 余额[" + account.getBalance() + "]"); account.setMark( BocAccount.Mark.NONE ); // this.notifyAll(); // 注意,这里的 this 指的是 当前的 Student 对象 ( 此时它并不是同步锁 ) System.out.println( Thread.currentThread().getName() + " - 没钱了,这是吃土的节奏啊" ); account.notifyAll(); // 谁是同步锁,就调用谁的 notifyAll 方法 } } catch (InterruptedException e) { e.printStackTrace(); } } } }
package ecut.threads; public class StudentDrawThread extends Thread { private Student student ; private double drawAmount ; public StudentDrawThread(Student student , double drawAmount , String threadName ) { super( threadName ); this.student = student; this.drawAmount = drawAmount ; } @Override public void run() { for( int i = 0 ; i < 2 ; i++ ){ student.draw(drawAmount); } System.out.println( "学生取钱线程结束" ); } }
package ecut.threads; /** * 家长 */ public class Patriarch { private BocAccount account; public Patriarch(BocAccount account) { super(); this.account = account; } public void deposit(double depositAmount) { synchronized ( account ) { try { if ( account.getMark() == BocAccount.Mark.RICH ) { System.out.println( Thread.currentThread().getName() + " : 卡上有钱,白跑一趟" ); // 哪个正在执行的线程调用 同步锁 的 wait 将导致 该线程进入到 阻塞状态 ( 等待池 ( wait pool ) ) // 该线程进入 等待池 时,同时 释放它所占有的 同步锁 ( 释放同步锁 ) account.wait(); // 谁是同步锁,就调用谁的 wait 方法 } else { System.out.println(Thread.currentThread().getName() + " - 存钱[" + depositAmount + "]"); account.setBalance( account.getBalance() + depositAmount ); System.out.println(Thread.currentThread().getName() + " - 余额[" + account.getBalance() + "]"); account.setMark( BocAccount.Mark.RICH ); System.out.println( Thread.currentThread().getName() + " - 又给臭小子把钱打过去了,我和老婆子这个月咋活呀" ); // 调用 同步锁的 notifyAll 方法,将唤醒 正在等待 (目前处于 等待池 中) 该同步锁的 所有线程 // 被唤醒的线程不是立即进入就绪状态,而是进入到 锁池 ( lock pool ) account.notifyAll(); } } catch (InterruptedException e) { e.printStackTrace(); } } } }
package ecut.threads; public class PatriarchDepositThread extends Thread { private Patriarch patriarch ; private double depositAmount ; public PatriarchDepositThread(Patriarch patriarch , double depositAmount , String threadName) { super( threadName ); this.patriarch = patriarch; this.depositAmount = depositAmount ; } @Override public void run() { for( int i = 0 ; i < 2 ; i++ ){ patriarch.deposit(depositAmount); } } }
package ecut.threads; public class Main { public static void main(String[] args) { // 创建一个 账号对象,它充当竞争资源 ( 同步锁 、对象监视器 ) final BocAccount account = new BocAccount( "家长" , 1000 ); // 创建一个 学生类型的对象 ,充当消费者 Student s = new Student( account ); // 创建一个 家长类型的对象,充当生产者 Patriarch p = new Patriarch( account ); // 哪个学生要取多少钱 StudentDrawThread st = new StudentDrawThread( s , 1000 , "学生" ); st.start(); // 哪位家长要存多少钱 PatriarchDepositThread pt = new PatriarchDepositThread( p , 1000 , "家长" ); pt.start(); } }
注意:线程最后的状态是在等待,即都处在等待池中,因为最后没有任何线程再调用同步锁的notify() 或者notifyAll() 来唤醒其中一个线程,最后获得运行机会的线程,因为执行了run中的代码,本质上是调用account 的一个同步方法,从而进入了等待池,而这之后没有线程再度唤醒所有的线程或一个线程最后的状态(被阻塞),是在等待池,不是死锁现象,这点必须注意!
运行结果如下:
学生 - 取钱[1000.0] 学生 - 余额[0.0] 学生 - 没钱了,这是吃土的节奏啊 学生 : 卡上没钱,白跑一趟 家长 - 存钱[1000.0] 家长 - 余额[1000.0] 家长 - 又给臭小子把钱打过去了,我和老婆子这个月咋活呀 家长 : 卡上有钱,白跑一趟 学生取钱线程结束
定时任务
1、定时器:定时器用于定时执行特定的任务
2、通过java.util.Timer 类的对象可以定时执行某项特定任务
- Timer 类本身没有继承Thread 也没有实现Runnable 接口
- 用于设定定时任务的常用方法schedule(TimerTask task, long delay, long period)
task : 即为需要定时执行的任务
delay : 表示延迟执行的时间(单位为毫秒)
period : 表示每次执行任务的间隔时间(单位为毫秒)
取消任务的方法:cancel()
3、所谓特定任务,是指java.util.TimerTask 类型的对象
- TimerTask 本身是个抽象类
- 想要得到一个TimerTask 类型的对象,必须使用TimerTask 的子类
- TimerTask 实现了Runnable 接口,但是没有实现其中的run() 方法
定时器测试案例一:
package ecut.threads; import java.util.Calendar; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerTest1 { private static Calendar calendar = Calendar.getInstance(); //静态代码块,在虚拟机加载类的时候就会加载执行,而且只执行一次; //非静态代码块,在创建对象的时候(即new一个对象的时候)执行,每次创建对象都会执行一次 static { calendar.set( Calendar.MILLISECOND , 0 ); } public static void main(String[] args) { Timer t = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { System.out.println( calendar.get( Calendar.MINUTE ) + ":" + calendar.get( Calendar.SECOND) ); t.cancel(); } } ; calendar.set( 2018 , 2 , 17, 14, 13, 00 );//14:13执行定时任务 Date time = calendar.getTime() ; t.schedule( task , time ); } }
运行结果如下:
13:0
定时器测试案例二:
package ecut.threads; import java.util.Calendar; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerTest2 { private static Calendar calendar = Calendar.getInstance(); static { calendar.set( Calendar.MILLISECOND , 0 ); } public static void main(String[] args) { final Timer t = new Timer(); //int n = 0 ;//jdk1.7 匿名内部类访问外部类的局部变量时,必须用final修饰,jdk1.8默认有final修饰 TimerTask task = new TimerTask() { int n = 0 ; @Override public void run() { System.out.println( n + " : " + System.currentTimeMillis() ); if( n >= 10 ){ t.cancel(); } else { n++ ; } } } ; calendar.set( 2018 , 2 , 17, 14, 38, 15 ); Date time = calendar.getTime() ; long period = 200 ;//每隔200ms执行一次 t.schedule( task , time , period ); } }
运行结果如下:
0 : 1521270071900 1 : 1521270072102 2 : 1521270072305 3 : 1521270072508 4 : 1521270072711 5 : 1521270072914 6 : 1521270073117 7 : 1521270073320 8 : 1521270073523 9 : 1521270073726 10 : 1521270073929
定时器测试案例三:
package ecut.threads; import java.util.Calendar; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerTest3 { private static Calendar calendar = Calendar.getInstance(); static { calendar.set( Calendar.MILLISECOND , 0 ); } public static void main(String[] args) { final Timer t = new Timer(); calendar.set( 2018 , 2 , 17, 14, 43, 40 ); Date time = calendar.getTime() ; TimerTask task1 = new TimerTask() { @Override public void run() { System.out.println( "播放音乐" ); } }; t.schedule( task1 , time , 1000 ); TimerTask task2 = new TimerTask() { @Override public void run() { System.out.println( "震动起来" ); //震动一次就取消掉 this.cancel(); // 取消当前的定时任务 t.purge(); // 从 定时器中移除已经取消的定时任务 } }; t.schedule( task2 , time , 1000 ); } }
运行结果如下:
播放音乐
震动起来
播放音乐
播放音乐
播放音乐
................
转载请于明显处标明出处