JavaSE学习总结(十八)—— 多线程
多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
一、多线程概要
1.1、多任务与单任务操作系统
多任务处理是指用户可以在同一时间内运行多个应用程序,每个应用程序被称作一个任务.Linux、windows就是支持多任务的操作系统,比起单任务系统它的功能增强了许多。
当多任务操作系统使用某种任务调度策略允许两个或更多进程并发共享一个处理器时,事实上处理器在某一时刻只会给一件任务提供服务。因为任务调度机制保证不同任务之间的切换速度十分迅速,因此给人多个任务同时运行的错觉。多任务系统中有3个功能单位:任务、进程和线程。
多任务处理有两种类型:
- 基于进程
- 基于线程
进程是指一种“自包容”的运行程序,有自己的地址空间;线程是进程内部单一的一个顺序控制流
基于进程的特点是允许计算机同时运行两个或更多的程序。
基于线程的多任务处理环境中,线程是最小的处理单位。
基于线程所需的开销更少
在多任务中,各个进程需要分配它们自己独立的地址空间
多个线程可共享相同的地址空间并且共同分享同一个进程
进程间调用涉及的开销比线程间通信多
线程间的切换成本比进程间切换成本低
1.2、进程与线程
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。
每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文.多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定. 线程的运行中需要使用计算机的内存资源和CPU。
在Java中,一个应用程序可以包含多个线程。每个线程执行特定的任务,并可与其他线程并发执行
多线程使系统的空转时间最少,提高CPU利用率、
多线程编程环境用方便的模型隐藏CPU在任务间切换的事实
在Java程序启动时,一个线程立刻运行,该线程通常称为程序的主线程。
主线程的重要性体现在两个方面:
它是产生其他子线程的线程。
通常它必须最后完成执行,因为它执行各种关闭动作。
class Mythread extends Thread { public static void main(String args[]) { Thread t= Thread.currentThread(); System.out.println("当前线程是: "+t); t.setName("MyJavaThread"); System.out.println("当前线程名是: "+t); try { for(int i=0;i<3;i++) { System.out.println(i); Thread.sleep(1500); } } catch(InterruptedException e) { System.out.println("主线程被中断"); } } }
1.3、多线程的优点
- 使用线程可以把占据时间长的程序中的任务放到后台去处理
- 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
- 程序的运行速度可能加快
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下可以释放一些珍贵的资源如内存占用等等。
- 多线程技术在IOS软件开发中也有举足轻重的位置。
- 线程应用的好处还有很多,就不一一说明了
一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。更为重要的是,由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。
1.4、多线程的缺点
- 如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。
- 更多的线程需要更多的内存空间。
- 线程可能会给程序带来更多“bug”,因此要小心使用。
- 线程的中止需要考虑其对程序运行的影响。
- 通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。
二、Timer和TimerTask
2.1. Timer和TimerTask
Timer是jdk中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。
TimerTask是一个实现了Runnable接口的抽象类,代表一个可以被Timer执行的任务。
2.2. 一个Timer调度的例子
Daemon()程序是一直运行的服务端程序,又称为守护进程。通常在系统后台运行,没有控制终端,不与前台交互,Daemon程序一般作为系统服务使用。Daemon是长时间运行的进程,通常在系统启动后就运行,在系统关闭时才结束。一般说Daemon程序在后台运行,是因为它没有控制终端,无法和前台的用户交互。Daemon程序一般都作为服务程序使用,等待客户端程序与它通信。我们也把运行的Daemon程序称作守护进程。
(1)Timer.schedule(TimerTask task,Date time)安排在制定的时间执行指定的任务。
(2)Timer.schedule(TimerTask task,Date firstTime ,long period)安排指定的任务在指定的时间开始进行重复的固定延迟执行.
(3)Timer.schedule(TimerTask task,long delay)安排在指定延迟后执行指定的任务.
(4)Timer.schedule(TimerTask task,long delay,long period)安排指定的任务从指定的延迟后开始进行重复的固定延迟执行.
(5)Timer.scheduleAtFixedRate(TimerTask task,Date firstTime,long period)安排指定的任务在指定的时间开始进行重复的固定速率执行.
(6)Timer.scheduleAtFixedRate(TimerTask task,long delay,long period)安排指定的任务在指定的延迟后开始进行重复的固定速率执行.
示例:
package com.zhangguo.thread; import java.util.Date; import java.util.Scanner; import java.util.Timer; import java.util.TimerTask; public class TimerDemo1 { public static void main(String[] args) { Timer timer1=new Timer("定时器1", false); //给定时器安排任务,延迟10毫秒执行,执行完后间隔3000毫秒执行 timer1.schedule(new TimerTask1("定时器A:"), 10, 3000); timer1.schedule(new TimerTask1("定时器B:"), 10, 1000); Scanner input=new Scanner(System.in); System.out.print("是否要结束任务:"); input.nextLine(); timer1.cancel(); //结束任务 System.out.println("定时任务结束了"); } } //定时任务 class TimerTask1 extends TimerTask{ private String taskName=""; public TimerTask1(String taskName) { this.taskName=taskName; } @Override public void run() { System.out.println(taskName+new Date()+""); } }
结果:
示例二:
package com.threaddemo; import java.util.Date; import java.util.Scanner; import java.util.Timer; import java.util.TimerTask; public class TimerSchedule { public static void main(String[] args) { Timer timer=new Timer(true); timer.schedule(new PrintTask(),0,3000); System.out.println("任意键结束"); Scanner input=new Scanner(System.in); input.nextLine(); timer.cancel(); System.out.println("结束了"); } } class PrintTask extends TimerTask{ @Override public void run() { System.out.println("时钟任务"+new Date().toString()); } }
示例三:
1 import java.util.Timer; 2 import java.util.TimerTask; 3 4 public class TestTimer { 5 6 public static void main(String args[]){ 7 System.out.println("About to schedule task."); 8 new Reminder(3); 9 System.out.println("Task scheduled."); 10 } 11 12 public static class Reminder{ 13 Timer timer; 14 15 public Reminder(int sec){ 16 timer = new Timer(); 17 timer.schedule(new TimerTask(){ 18 public void run(){ 19 System.out.println("Time's up!"); 20 timer.cancel(); 21 } 22 }, sec*1000); 23 } 24 } 25 }
运行之后,在console会首先看到:
About to schedule task.
Task scheduled.
然后3秒钟后,看到
Time's up!
从这个例子可以看出一个典型的利用timer执行计划任务的过程如下:
- new一个TimerTask的子类,重写run方法来指定具体的任务,在这个例子里,我用匿名内部类的方式来实现了一个TimerTask的子类
- new一个Timer类,Timer的构造函数里会起一个单独的线程来执行计划任务。jdk的实现代码如下:
1 public Timer() { 2 this("Timer-" + serialNumber()); 3 } 4 5 public Timer(String name) { 6 thread.setName(name); 7 thread.start(); 8 }
- 调用相关调度方法执行计划。这个例子调用的是schedule方法。
- 任务完成,结束线程。这个例子是调用cancel方法结束线程。
2.3. 如何终止Timer线程
默认情况下,创建的timer线程会一直执行,主要有下面四种方式来终止timer线程:
- 调用timer的cancle方法
- 把timer线程设置成daemon线程,(new Timer(true)创建daemon线程),在jvm里,如果所有用户线程结束,那么守护线程也会被终止,不过这种方法一般不用。
- 当所有任务执行结束后,删除对应timer对象的引用,线程也会被终止。
- 调用System.exit方法终止程序
2.4. 关于cancle方式终止线程
这种方式终止timer线程,jdk的实现比较巧妙,稍微说一下。
首先看cancle方法的源码:
1 public void cancel() { 2 synchronized(queue) { 3 thread.newTasksMayBeScheduled = false; 4 queue.clear(); 5 queue.notify(); // In case queue was already empty. 6 } 7 }
没有显式的线程stop方法,而是调用了queue的clear方法和queue的notify方法,clear是个自定义方法,notify是Objec自带的方法,很明显是去唤醒wait方法的。
再看clear方法:
1 void clear() { 2 // Null out task references to prevent memory leak 3 for (int i=1; i<=size; i++) 4 queue[i] = null; 5 6 size = 0; 7 }
clear方法很简单,就是去清空queue,queue是一个TimerTask的数组,然后把queue的size重置成0,变成empty.还是没有看到显式的停止线程方法,回到最开始new Timer的时候,看看new Timer代码:
1 public Timer() { 2 this("Timer-" + serialNumber()); 3 } 4 5 public Timer(String name) { 6 thread.setName(name); 7 thread.start(); 8 }
看看这个内部变量thread:
1 /** 2 * The timer thread. 3 */ 4 private TimerThread thread = new TimerThread(queue);
不是原生的Thread,是自定义的类TimerThread.这个类实现了Thread类,重写了run方法,如下:
1 public void run() { 2 try { 3 mainLoop(); 4 } finally { 5 // Someone killed this Thread, behave as if Timer cancelled 6 synchronized(queue) { 7 newTasksMayBeScheduled = false; 8 queue.clear(); // Eliminate obsolete references 9 } 10 } 11 }
最后是这个mainLoop方法,这方法比较长,截取开头一段:
1 private void mainLoop() { 2 while (true) { 3 try { 4 TimerTask task; 5 boolean taskFired; 6 synchronized(queue) { 7 // Wait for queue to become non-empty 8 while (queue.isEmpty() && newTasksMayBeScheduled) 9 queue.wait(); 10 if (queue.isEmpty()) 11 break; // Queue is empty and will forever remain; die
可以看到wait方法,之前的notify就是通知到这个wait,然后clear方法在notify之前做了清空数组的操作,所以会break,线程执行结束,退出。
2.5. 反复执行一个任务
通过调用三个参数的schedule方法实现,最后一个参数是执行间隔,单位毫秒。
2.6. schedule VS. scheduleAtFixedRate
这两个方法都是任务调度方法,他们之间区别是,schedule会保证任务的间隔是按照定义的period参数严格执行的,如果某一次调度时间比较长,那么后面的时间会顺延,保证调度间隔都是period,而scheduleAtFixedRate是严格按照调度时间来的,如果某次调度时间太长了,那么会通过缩短间隔的方式保证下一次调度在预定时间执行。举个栗子:你每个3秒调度一次,那么正常就是0,3,6,9s这样的时间,如果第二次调度花了2s的时间,如果是schedule,就会变成0,3+2,8,11这样的时间,保证间隔,而scheduleAtFixedRate就会变成0,3+2,6,9,压缩间隔,保证调度时间。
2.7. 一些注意点
- 每一个Timer仅对应唯一一个线程。
- Timer不保证任务执行的十分精确。
- Timer类的线程安全的。
三、多线程的实现方式
通过以下两种方法创建 Thread 对象:
3.1、继承Thread
Java中“一切皆对象”,线程也被封装成一个对象。我们可以通过继承Thread类来创建线程。线程类中的的run()方法包含了该线程应该执行的指令。我们在衍生类中覆盖该方法,以便向线程说明要做的任务:
声明一个 Thread 类的子类,并覆盖 run() 方法。
class mythread extends Thread { public void run( ) {/* 覆盖该方法*/ } }
这里继承Thread类的方法是比较常用的一种,如果说你只是想起一条线程。没有什么其它特殊的要求,那么可以使用Thread.(笔者推荐使用Runable,后头会说明为什么)。下面来看一个简单的实例
package com.zhangguo.thread; public class ThreadDemo1 { public static void main(String[] args) { System.out.println("线程示例开始"); //创建线程对象 MyThread threadA=new MyThread("线程A"); MyThread threadB=new MyThread("线程B"); //开始运行 threadA.start(); threadB.start(); System.out.println("线程示例结束"); } } /**定义线程类*/ class MyThread extends Thread { private String name; public MyThread(String name) { this.name=name; } @Override public void run() { for (int i = 1; i <= 10; i++) { System.out.println(this.name+":"+i); } } }
结果1:
线程示例开始 线程示例结束 线程A:1 线程A:2 线程B:1 线程B:2 线程A:3 线程A:4 线程A:5 线程A:6 线程B:3 线程B:4 线程A:7 线程A:8 线程A:9 线程A:10 线程B:5 线程B:6 线程B:7 线程B:8 线程B:9 线程B:10
结果2:
线程示例开始 线程示例结束 线程A:1 线程A:2 线程A:3 线程A:4 线程A:5 线程A:6 线程B:1 线程B:2 线程B:3 线程B:4 线程B:5 线程B:6 线程B:7 线程B:8 线程B:9 线程B:10 线程A:7 线程A:8 线程A:9 线程A:10
说明:
程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MyThread的两个对象的start方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。
注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
Thread.sleep()方法调用目的是不让当前线程休眠一段时间,暂停运行。
实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
3.2、实现Runnable 接口
实现多线程的另一个方式是实施Runnable接口,并提供run()方法。实施接口的好处是容易实现多重继承(multiple inheritance)。然而,由于内部类语法,继承Thread创建线程可以实现类似的功能。我们在下面给出一个简单的例子,而不深入:
声明一个实现 Runnable 接口的类,并实现 run() 方法。
class mythread implements Runnable{ public void run( ) {/* 实现该方法*/ }
}
采用Runnable也是非常常见的一种,我们只需要重写run方法即可。下面也来看个实例。
示例:
package com.zhangguo.thread; public class RunableDemo { public static void main(String[] args) { System.out.println("线程示例开始"); //创建线程对象 Thread threadA=new Thread(new MyRunnable("线程A")); Thread threadB=new Thread(new MyRunnable("线程B")); threadA.start(); threadB.start(); System.out.println("线程示例结束"); } } /**定义线程类*/ class MyRunnable implements Runnable { private String name; public MyRunnable(String name) { this.name=name; } @Override public void run() { for (int i = 1; i <= 10; i++) { System.out.println(this.name+":"+i); } } }
结果1:
线程示例开始 线程A:1 线程A:2 线程A:3 线程A:4 线程示例结束 线程A:5 线程A:6 线程A:7 线程A:8 线程A:9 线程A:10 线程B:1 线程B:2 线程B:3 线程B:4 线程B:5 线程B:6 线程B:7 线程B:8 线程B:9 线程B:10
结果2:
线程示例开始 线程A:1 线程A:2 线程A:3 线程A:4 线程A:5 线程A:6 线程A:7 线程A:8 线程A:9 线程A:10 线程示例结束 线程B:1 线程B:2 线程B:3 线程B:4 线程B:5 线程B:6 线程B:7 线程B:8 线程B:9 线程B:10
说明:
Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
思考题:
package com.zhangguo.thread; public class RunnableDemo2 { public int n=0; public static void main(String[] args) { RunnableDemo2 obj=new RunnableDemo2(); new Thread(new Counter(obj)).start(); new Thread(new Counter(obj)).start(); new Thread(new Counter(obj)).start(); System.out.println(obj.n); } } class Counter implements Runnable{ RunnableDemo2 obj; public Counter(RunnableDemo2 obj) { this.obj=obj; } @Override public void run() { for (int i = 0; i < 20000; i++) { obj.n++; } } }
结果1:
5796
结果2:
4615
结果3:
20000
结果为什么是不确认的,怎样保证正确的结果。
3.3、Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
提醒一下大家:main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。
四、线程状态与调度
4.1、线程的状态
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
4.2、线程调度
线程的调度
1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
分配给线程的默认优先级,取值为5。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
3、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
4、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
6、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
4.3、线程类的一些常用方法
sleep(): 强迫一个线程睡眠N毫秒。
isAlive(): 判断一个线程是否存活。
join(): 等待线程终止。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级
五、线程同步
在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。在Java中可用synchronized关键字。
5.1、线程同步的问题
package com.zhangguo.thread; public class RunnableDemo2 { public static void main(String[] args) throws Exception { NumberBox obj=new NumberBox(); Thread t1=new Thread(new Counter(obj)); Thread t2=new Thread(new Counter(obj)); Thread t3=new Thread(new Counter(obj)); t1.start(); t2.start(); t3.start(); Thread.sleep(5000); System.out.println(obj.n); } } class NumberBox{ public int n=0; } class Counter implements Runnable{ NumberBox obj; public Counter(NumberBox obj) { this.obj=obj; } @Override public void run() { for (int i = 0; i < 20000; i++) { obj.n=obj.n+1; } System.out.println("n="+obj.n); } }
结果:
n=20226 n=30328 n=50328 50328
因为obj.n=obj.n+1是并行执行的,资源存在争用的问题,需要同步,否则结果不正确。
5.2、synchronized
5.2.1、同步方法
synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
public synchronized void save(){ }
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
示例:
package com.zhangguo.thread.sync; public class SyncDemo2 { public static void main(String[] args) throws Exception { Box obj = new Box(); /*多线程单实例*/ Inc incObj=new Inc(obj); Thread t1 = new Thread(incObj); Thread t2 = new Thread(incObj); Thread t3 = new Thread(incObj); t1.start(); t2.start(); t3.start(); } } class Box { public int n = 0; } class Inc implements Runnable { Box obj; public Inc(Box obj) { this.obj = obj; } @Override public void run() { counter(); } /** 同步方法,锁的是调用该方法的对象 */ public synchronized void counter() { for (int i = 0; i < 20000; i++) { obj.n = obj.n + 1; } System.out.println("n=" + obj.n); } }
结果:
n=20000 n=40000 n=60000
5.2.2、同步代码块
synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
代码如:
synchronized(object){ }
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
示例:
package com.zhangguo.thread; public class SyncDemo1 { public static void main(String[] args) throws Exception { Box obj=new Box(); Thread t1=new Thread(new Inc(obj)); Thread t2=new Thread(new Inc(obj)); Thread t3=new Thread(new Inc(obj)); t1.start(); t2.start(); t3.start(); } } class Box{ public int n=0; } class Inc implements Runnable{ Box obj; public Inc(Box obj) { this.obj=obj; } @Override public void run() { for (int i = 0; i < 20000; i++) { synchronized (obj) { obj.n=obj.n+1; } } System.out.println("n="+obj.n); } }
结果:
n=43533 n=52590 n=60000
5.3、使用特殊域变量(volatile)实现线程同步
['vɑːlətl] 不稳定的;反复无常的;易挥发的
a.volatile关键字为域变量的访问提供了一种免锁机制,
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
//只给出要修改的代码,其余代码与上同 class Bank { //需要同步的变量加上volatile private volatile int account = 100; public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { account += money; } }
注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。
用final域,有锁保护的域和volatile域可以避免非同步的问题。
在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。
示例:
package com.zhangguo.thread.syncv; public class SyncDemo3 { public static void main(String[] args) throws Exception { /* 多线程单实例 */ Inc incObj = new Inc(); Thread t1 = new Thread(incObj); Thread t2 = new Thread(incObj); Thread t3 = new Thread(incObj); t1.start(); t2.start(); t3.start(); while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(incObj.n); } } class Inc implements Runnable { public volatile int n = 0; @Override public void run() { counter(); } public void counter() { for (int i = 0; i < 20000; i++) { this.n = this.n + 1; } System.out.println("n=" + this.n); } }
结果:
n=34329 n=38177 n=33705 38177
所以,volatile并不能保证数据是同步的,只能保证线程得到的数据是最新的。
那么,我们应该在什么情况下使用volatile关键字呢?
其实很简单.只要符合以下两个条件就能使用volatile,并且能收到很不错的效果。
1.对变量的写入不依赖变量的当前值,或者只有一个线程更新变量的值
2.该变量不会和其他状态变量一起被列为不变性条件中
5.4.使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,
它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
代码实例:
package com.zhangguo.thread.syncv; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SyncDemo3 { public static void main(String[] args) throws Exception { /* 多线程单实例 */ Inc incObj = new Inc(); Thread t1 = new Thread(incObj); Thread t2 = new Thread(incObj); Thread t3 = new Thread(incObj); t1.start(); t2.start(); t3.start(); while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(incObj.n); } } class Inc implements Runnable { public int n = 0; private Lock lock = new ReentrantLock(); @Override public void run() { counter(); } public void counter() { for (int i = 0; i < 20000; i++) { // 锁 lock.lock(); try { this.n = this.n + 1; } finally { lock.unlock(); } } System.out.println("n=" + this.n); } }
结果:
n=26058 n=45353 n=60000 60000
注:关于Lock对象和synchronized关键字的选择:
a.最好两个都不用,使用一种java.util.concurrent包提供的机制,
能够帮助用户处理所有与锁相关的代码。
b.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
c.如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
5.5.使用局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,
副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal 类的常用方法
ThreadLocal() : 创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的"初始值"
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
例如:
在上面例子基础上,修改后的代码为:
代码实例:
//只改Bank类,其余代码与上同 public class Bank{ //使用ThreadLocal类管理共享变量account private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){ @Override protected Integer initialValue(){ return 100; } }; public void save(int money){ account.set(account.get()+money); } public int getAccount(){ return account.get(); } }
注:ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式
5.6.使用阻塞队列实现线程同步
前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。
使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。
本小节主要是使用
LinkedBlockingQueue<E>
来实现线程的同步
LinkedBlockingQueue<E>是一个基于已连接节点的,范围任意的blocking queue。
队列是先进先出的顺序(FIFO),关于队列以后会详细讲解~
LinkedBlockingQueue 类常用方法
LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
put(E e) : 在队尾添加一个元素,如果队列满则阻塞
size() : 返回队列中的元素个数
take() : 移除并返回队头元素,如果队列空则阻塞
代码实例:
实现商家生产商品和买卖商品的同步
1 package com.xhj.thread; 2 3 import java.util.Random; 4 import java.util.concurrent.LinkedBlockingQueue; 5 6 /** 7 * 用阻塞队列实现线程同步 LinkedBlockingQueue的使用 8 * 9 * @author XIEHEJUN 10 * 11 */ 12 public class BlockingSynchronizedThread { 13 /** 14 * 定义一个阻塞队列用来存储生产出来的商品 15 */ 16 private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(); 17 /** 18 * 定义生产商品个数 19 */ 20 private static final int size = 10; 21 /** 22 * 定义启动线程的标志,为0时,启动生产商品的线程;为1时,启动消费商品的线程 23 */ 24 private int flag = 0; 25 26 private class LinkBlockThread implements Runnable { 27 @Override 28 public void run() { 29 int new_flag = flag++; 30 System.out.println("启动线程 " + new_flag); 31 if (new_flag == 0) { 32 for (int i = 0; i < size; i++) { 33 int b = new Random().nextInt(255); 34 System.out.println("生产商品:" + b + "号"); 35 try { 36 queue.put(b); 37 } catch (InterruptedException e) { 38 // TODO Auto-generated catch block 39 e.printStackTrace(); 40 } 41 System.out.println("仓库中还有商品:" + queue.size() + "个"); 42 try { 43 Thread.sleep(100); 44 } catch (InterruptedException e) { 45 // TODO Auto-generated catch block 46 e.printStackTrace(); 47 } 48 } 49 } else { 50 for (int i = 0; i < size / 2; i++) { 51 try { 52 int n = queue.take(); 53 System.out.println("消费者买去了" + n + "号商品"); 54 } catch (InterruptedException e) { 55 // TODO Auto-generated catch block 56 e.printStackTrace(); 57 } 58 System.out.println("仓库中还有商品:" + queue.size() + "个"); 59 try { 60 Thread.sleep(100); 61 } catch (Exception e) { 62 // TODO: handle exception 63 } 64 } 65 } 66 } 67 } 68 69 public static void main(String[] args) { 70 BlockingSynchronizedThread bst = new BlockingSynchronizedThread(); 71 LinkBlockThread lbt = bst.new LinkBlockThread(); 72 Thread thread1 = new Thread(lbt); 73 Thread thread2 = new Thread(lbt); 74 thread1.start(); 75 thread2.start(); 76 77 } 78 79 }
注:BlockingQueue<E>定义了阻塞队列的常用方法,尤其是三种添加元素的方法,我们要多加注意,当队列满时:
add()方法会抛出异常
offer()方法返回false
put()方法会阻塞
5.7.使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即-这几种行为要么同时完成,要么都不完成。
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中AtomicInteger
表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值
代码实例:
只改Bank类,其余代码与上面第一个例子同
1 class Bank { 2 private AtomicInteger account = new AtomicInteger(100); 3 4 public AtomicInteger getAccount() { 5 return account; 6 } 7 8 public void save(int money) { 9 account.addAndGet(money); 10 } 11 }
补充--原子操作主要有:
对于引用变量和大多数原始变量(long和double除外)的读写操作;
对于所有使用volatile修饰的变量(包括long和double)的读写操作。
六、作业
6.1、每隔5秒向c:\logs目录下写入一个文件(yyyyMMddHHmmss.txt),内容为一个GUID,总文件数不超过100个,保留最新的文件。
6.2、在项目中增加一个数据库自动备份功能,每5个小时备份一次,可以删除备份文件。
在监听器的contextInitialized事件中初始化任务
6.3、实现1+2+3+.....1000000,使用多线程分段累加后合并结果,并测试使用单线程与多线程所耗时的差别。