Table of Contents
Java多线程
1:进程与线程
1.1:概念
1.2:java线程的运行机制
2:线程的创建与启动
2.1:拓展Thread类
2.1.1:创建线程
2.1.2:sleep方法
2.2:实现Runnable接口
3:线程状态的转换
3.1:新建状态
3.2:就绪状态
3.3:运行状态
3.4:阻塞状态
3.5:死亡状态
4:线程调度
4.1:调整各个线程的优先级
4.2:线程睡眠:Thread.sleep()
4.3:线程让步:Thread.yield()
4.4:等待其他线程结束:其他线程对象.join()---让别人先走,走完自己再走
4.5:废弃的停止线程方法:stop()
4.6:中断线程 : thread.interrupt()
4.6.1 使用中断信号量中断非阻塞状态的线程
4.6.2 中断阻塞状态线程
5:查看线程
5.1 获取当前线程的引用
5.2:查看所有的活动
6:后台进程
7:定时器
7.1:ScheduledExecutorService---多线程池的执行任务,有线程池 可以支持多个任务并发执行
7.1.1:ScheduledFuture schedule(Runnable command,long delay,TimeUnit unit)
7.1.2:ScheduledFuture scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
7.1.3:ScheduledFuture scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)
7.2:Timer---单线程(当执行任务的时间间隔小于执行任务的时间, timer就会等待上一个任务执行结束才执行下一个)
7.3:ScheduledExecutorService的使用
7.2.1:基本使用
7.2.2:schedule Runnable
7.2.3:schedule Callable
8:线程的同步
8.1:同步代码块
8.2:同步方法
8.3:同步与并发
8.4:线程安全的类
几乎每种操作系统都支持进程的概念——进程就是在某种程度上相互隔离的、独立运行的程序,每一个进程都有自己独立的内存空间。比如 IE 浏览器程序,每打开一个 IE 浏览器窗口,就启动一个新的进程。在 java 中,我们执行 java.exe 程序,就启动一个独立的 Java 虚拟机进程,该进程的任务就是解析并执行 Java 程序代码。
线程是指进程中的一个执行流程,一个进程可以由多个线程组成,即一个进程中可以同时运行多个不同的线程,它们分别执行不同的任务。当进程内的多个线程同时运行时,这种运行方式成为并发运行。
线程又被称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统进行调度。线程和进程的区别是:
许多服务器程序,如数据库服务器和 Web 服务器,都支持并发运行,这些服务器能同时响应来自不同客户的请求。
在 java 虚拟机进程中,执行程序代码的任务是由线程来完成的。每个线程都有一个独立的程序计数器和方法调用栈。
程序计数器:也称为 PC 寄存器,当线程执行一个方法时,程序计数器指向方法区中下一条要执行的字节码指令。
方法调用栈:简称方法栈,用来跟踪线程运行中一系列的方法调用过程,栈中的元素称为栈帧,每当线程调用一个方法的时候,就会向方法栈压入一个新帧。帧用来存储方法的参数、局部变量和运算过程中的临时数据。
栈帧由以下三个部分组成:
每当用 Java 命令启动一个 Java 虚拟机进程时,Java 虚拟机都会创建一个主线程,该线程从程序入口 main()方法开始执行。以下面程序为例,介绍线程的运行过程。
public class Test {
private int num; //实例变量
public int add(){
int b=0; //局部变量
num++;
b=num;
return b;
}
public static void main(String[] args) {
Test t=new Test(); //局部变量
int num=0; //局部变量
num=t.add();
System.out.println(num);
}
}
主线程从 main()方法的程序代码开始运行,当它开始执行 method()方法的“a++”操作时,运行时数据区的状态如下图所示。
当主线程执行“a++”操作时,它能根据 method()方法的栈帧的栈数据区中的有关信息,正确地定位到堆区的 Test 对象的实例变量 num,并把它的值加 1。
当 add()方法执行完毕后,它的栈帧就会从方法栈中弹出,它的局部变量 b 结束生命周期。main()方法的栈帧就成为当前帧,主线程继续执行 main()方法。
方法区存放了线程所执行的字节码指令,堆区存放了线程所操作的数据(以对象的形式存放),Java 栈区则是线程的工作区,保存线程的运行状态。
在Java中可以通过 Thread 类的 currentThread 方法得到正在调用该方法的线程;
public class TestStream {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(""+thread.getName());//main
}
}
Java 在代码中对线程进行了支持,程序员可以创建自己的线程,它将和主线程并发运行。创建线程有两种方式:
Thread 类代表线程类,它的最主要的两个方法是:
开发线程类只需要继承 Thread 类,覆盖Thread 类的 run()方法即可。在Thread 类中,run()方法的定义如下:public void run()
public class MyThread extends Thread{
@Override
public void run() {
//线程中的实现
for (int i = 0; i <100 ; i++) {
System.out.println(""+this.getName()+"---"+i);
}
}
}
public class TestMain {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println(""+Thread.currentThread().getName());
}
}
当执行“java MyThread“命令时,Java 虚拟机首先创建并启动主线程。主线程的任务是执行 main 方法,main 方法创建了一个 MyThread 对象,然后调用它的 start()方法启动MyThread 线程。MyThread 线程的任务是执行它的 run()方法。
主线程和自定义线程并发运行
在下面,创建两个MyThread线程并发运行
当主线程执行 main()方法时,会创建两个 MyThread 对象,然后启动两个 MyThread 线程。在 Java 虚拟机钟有两个线程并发执行 MyThread 对象的 run()方法。在两个线程各自的方法栈中都有代表 run()方法的栈帧,在这个帧中存放了局部变量 num,也就是每个线程都拥有自己的局部变量 num,它们都分别从 0 增加到 100。
因为 Thread 类中有 getName()方法,MyThread 类继承了 Thread 类,所以在代码中可以使用 this.getName()得到当前线程的名字。mt 对象启动线程的名字是 Thread-0,mt2 对象启动线程的名字是 Thread-1。以此类推;
Thread类中有一个sleep方法,在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),就是线程睡眠一定的时间,也就是交出 CPU 时间片,根据参数来决定暂停时间长度,让给等待序列中的下一个线程 。 Sleep 方法抛出 InterruptedException 。
public class MyThread extends Thread{
@Override
public void run() {
//线程中的实现
for (int i = 0; i <100 ; i++) {
//当前线程休眠100毫秒
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(""+this.getName()+"---"+i);
}
}
}
public class TestMain {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
System.out.println(""+Thread.currentThread().getName());
}
}
结果显示:
当 Thread-0 线程执行打印后,休眠 100 毫秒,也就失去了 CPU 的时间片,Thread-1 线程就得到了CPU的时间片,执行了打印操作,也休眠100毫秒,100毫秒后,Thread-0线程先恢复到可运行状态,接着运行,这样两个线程交替运行。
不要随便覆盖 Thread 类的 start() 方法,创建一个线程对象后,线程并不自动开始运行,必须调用它的 start()方法才能启动线程。JDK 为 Thread 类的 start()方法提供了默认的实现,启动线程后调用 run()方法。如果不通过 start()方法启动线程,而是直接调用 run()方法,那只是普通的方法调用,
在 Thread 子类中不要随意覆盖 start()方法,假如一定要覆盖 start()方法,那么应该先调用 super.start()方法。
一个线程只能被启动一次
一个线程只能被启动一次,以下代码视图两次启动 MyThread 线程。
MyThread mt=new MyThread();
mt.start();
mt.start(); //抛出IllegalThreadStateException异常
Java 类不允许一个类继承多个类,因此一旦一个类继承了 Thread 类,就不能再继承其他的类,为了解决这一问题,Java 提供了 java.lang.Runnable 接口,它有一个 run()方法,定义如下:public void run()
public class MyThread implements Runnable {
int count = 0;
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
if (count > 10) {
break;//当count大于 10 的时候,循环结束
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TestMain {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
thread1.start();
thread2.start();
}
}
在 Thread 类中定义了如下形式的构造方法:public Thread(Runnable runnable)
当线程启动时,将执行参数runnable所引用对象的run()方法。其实Thread类也实现了Runnable 接口。
在示例中,主线程创建了 t1 和 t2 两个线程对象。启动 t1 和 t2 线程将执行 MyThread 对象的 run()方法。t1 和 t2 共享同一个 MyThread 对象,在执行 run()方法时将操作同一个实例变量 count。
也可以用匿名内部类
public class TestMain {
public static void main(String[] args) {
new Thread(new Runnable() {
int count = 0;
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
if (count > 10) {
break;//当count大于 10 的时候,循环结束
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
用 new 语句创建的线程对象处于新建状态,此时和其他 Java 对象一样,仅仅在堆区中被分配了内存。
当一个线程对象创建后,其他线程调用它的 start()方法,该线程就进入了就绪状态,Java 虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得 CPU 的使用权。
处于这个状态的线程占用 CPU,执行程序代码。在并发运行环境中,如果计算机只有一个CPU,那么任何时刻只会有一个线程处于这个状态。如果计算机有多个 CPU,那么同一时刻可以让几个线程占用不同的 CPU,使它们都处于运行状态。只有处于就绪状态的线程才有机会转到运行状态。
塞状态是指线程因为某些原因放弃 CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配 CPU,直到线程重新进入就绪状态,才有机会转到运行状态。
阻塞状态可以分为以下 3 种:
当线程退出 run(方法)时,就进入死亡状态,表示该线程结束生命周期。线程有可能是正常执行完 run()方法而退出的,也有可能是遇到异常而退出。不管线程正常结束还是异常结束,都不会对其他线程造成影响。
计算机通常只有一个 CPU,在任何时刻只能执行一条机器指令,每个线程只有获得 CPU 的使用权才能执行指令。所谓多线程的并发,其实是指宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在可运行池中,会有多个处于就绪状态的线程等待 CPU,Java 虚拟机的一项任务就是负责线程的调度。线程的调度是指按照特定的机制为多个线程分配 CPU 的使用权。
有两种调度模型: 分时调度模型和抢占式调度模型。
分时调度模型是指让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。
Java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU,如果可运行池中线程的优先级相同,那么就随机选择一个线程,使其占用 CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。一个线程会因为以下原因而放弃 CPU:
值得注意的是,线程的调度不是跨平台的,它不仅取决于 Java 虚拟机,还依赖操作系统。在某些操作系统中,只要运行中的线程没有遇到阻塞,也会在运行一段时间后放弃 CPU,给其他线程运行的机会。
在 java 中,同时启动多个线程后,不能保证各个线程轮流获得均等的 CPU 时间片,从之前的例子中,大家可以体会到这一点。一个线程运行机毫秒后,就放弃的 CPU 时间片,另一个线程就得到了 CPU 时间片,各个线程交替运行。
所有处于就绪状态的线程根据优先级存放在可运行池中,优先级低的线程获得较少的运行机会 , 优 先 级 高 的 线 程 获 得 较 多 的 运 行 机 会 。 Thread 类 的 setPriority(int) 和getPriority()方法分别用来设置优先级和读取优先级。优先级用整数表示,取值范围是1~10,Thread 类有以下 3 个静态常量。
如果不设置线程的优先级,线程默认的优先级为 5。
值得注意的是,尽管Java提供了10个优先级,但它与多数操作系统都不能很好地映射。比如 Windows2000 有 7个优先级,并且不是固定的,而 Sun公司的 Solaris 操作系统有 2的 31次方个优先级。如果希望程序能移植到各个操作系统中,应该确保在设置线程的优先级时,只使用 MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY 这 3 个优先级。这样才能保证在不同的操作系统中,对同样优先级的线程采用同样的调度方式。
当一个线程在运行过程中执行了sleep()方法时,它就会放弃CPU,转到阻塞状态。下面示例中每执行一次循环,就睡眠 1000 毫秒。
public class TestMain {
public static void main(String[] args) {
new Thread(new Runnable() {
int count = 0;
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
try {
System.out.println(""+i);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
Thread 类的 sleep(long millis)方法是静态的,millis参数设定睡眠的时间,以毫秒为单位。当执行 sleep()方法时,就会放弃 CPU 开始睡眠,2 秒钟后线程结束睡眠,就会获得CPU,继续进行下一次循环。所以会感觉程序运行很慢。
值得注意的是,当某线程结束睡眠后,首先转到就绪状态,假如其他的线程正在占用 CPU,那么该线程就在可运行池中等待获得 CPU。
当线程在运行中执行了Thread类的yield()静态方法,如果此时具有相同优先级的其他线程处于就绪状态,那么 yield()方法将把当前运行的线程放到可运行池中并使另一个线程运行。如果没有相同优先级的可运行线程,则 yield()方法什么也不做。
总的来说只让给同优先级的
class MyThread extends Thread {
int count = 0;
public void run() {
while (true) {
System.out.println(getName() + ":" + count++);
yield();
}
}
}
class Client {
public static void main(String[] arr) throws InterruptedException {
MyThread mt=new MyThread();
MyThread mt2=new MyThread();
mt.start();
mt2.start();
}
}
sleep()方法和 yield()方法都是 Thread 类的静态方法,都会使当前处于运行状态的线程放弃 CPU,把运行机会让给其他线程。两者的区别在于:
sleep()方法会给其他线程运行机会,而不考虑其他线程的优先级,因此会给较低优先级线程一个机会;yield()方法只会给相同优先级或者更高优先级线程一个运行的机会。
当线程执行了 sleep(long millis)方法后,会转到阻塞状态,参数 millis 指定睡眠的时间;当线程执行了 yield()方法后,将转到就绪状态。
sleep()方法方法抛出InterrupedException异常,而yield()方法没有声明抛出任何异常。
sleep()方法比 yield()方法具有更好的可移植性。不能依靠 yield()方法来提高程序的并发性能。对于大多数程序员来说,yield()方法的唯一用途是在测试期间人为地提高程序的并发性能,以帮助发现一些隐藏的错误。
当前运行的线程可以调用另一个线程的 join()方法,当前运行的线程将转到阻塞状态,直至另一个线程运行结束,它才恢复运行。
join()方法有两种重载形式:
timeout 参数设定当前线程被阻塞的时间,以毫秒为单位。如果把示例 main()方法中的mt.join() 改为 mt.join(10) ,那么当主线程被阻塞的时间超过了 10 毫秒,或者 mt 线程运行结束时,主线程就会恢复运行。
stop 这个方法是臭名昭著了,早就被弃用了,但是现在任然有很多钟情与他的人,永远都放不下他,因为从他的字面意思上我们可以知道他貌似可以停止一个线程,这个需求是每个搞线程开发的人都想要的操作,但是他并非是真正意义上的停止线程,而且停止线程还会引来一些其他的麻烦事,下面就来详细的介绍一下这个方法的:
从 SUN 的官方文档可以得知,调用 Thread.stop()方法是不安全的,这是因为当调用Thread.stop()方法时,会发生下面两件事:
线程的 thread.interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为 true)。它并不像 stop 方法那样会中断一个正在运行的线程。
判断某个线程是否已被发送过中断请求,请使用Thread.currentThread().isInterrupted()方法(因为它将线程中断标示位设置为true后,不会立刻清除中断标示位,即不会将中断标设置为false),而不要使用thread.interrupted()(该方法调用后会将中断标示位清除,即重新设置为false)方法来判断,下面是线程在循环中时的中断方式:
中断线程最好的,最受推荐的方式是,使用共享变量(shared variable)发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量,然后有秩序地中止任务。下面代码描述了这一方式:
public class MyThread extends Thread{
int count = 0;
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
//这里检测到中断位的话,该线程就终止,并复位改中断位
if(Thread.currentThread().isInterrupted()){
break;
}
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
public class TestMain {
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
mt.start();
System.out.println("main ...");
for (int i = 0; i <100 ; i++) {
System.out.println("main"+i);
if(i==80){
//这里将mt线程设置中断位,当线程检测到中断位后,mt线程终止
mt.interrupt();
}
}
}
}
Thread.interrupt()方法不会中断一个非正在运行的线程。这一方法实际上完成的是,设置线程的中断标示位,在线程受到阻塞的地方(如调用 sleep、wait、join 等地方)抛出一个异常 InterruptedException,并且中断状态也将被清除,这样线程就得以退出阻塞的状态。
主线程执行Thread对象的interrupt方法,线程可以捕获到中断,立即抛出异常,停止循环,在catch中结束线程。
如果是wait方法休眠,同样会抛出InterruptedException异常。
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//如果在休眠状态下该线程被调用.interrupt()方法,将被抛出异常,我们采用在这里终止
e.printStackTrace();
break;
}
}
}
}
public class TestMain {
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
mt.start();
System.out.println("main ...");
for (int i = 0; i <100 ; i++) {
System.out.println("main"+i);
if(i==80){
//这里将mt线程设置中断位,当线程检测到中断位后,mt线程终止
mt.interrupt();
}
}
}
}
Thread 类的 currentThread()静态方法返回当前线程对象的引用。在主线程 main 中执行 currentThread()方法时,返回主线程对象的引用。
public class TestMain {
public static void main(String[] args) throws InterruptedException {
Thread main = Thread.currentThread();
//输出当前线程的名称
System.out.println(""+main.getName());//main
}
}
Thread.getAllStackTraces()方法用于获取虚拟机中所有线程的 StackTraceElement 对象。在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈,代码如下:
public class TestMain {
public static void main(String[] args) throws InterruptedException {
//创建线程对象
MyThread t1=new MyThread();
MyThread t2=new MyThread();
//启动线程
t1.start();
t2.start();
Map allStackTraces = Thread.getAllStackTraces();
Set threads = allStackTraces.keySet();
//打印所有线程的名字
threads.stream().forEach(x->System.out.println("thread:"+x.getName()));
}
}
后台线程是指为其他线程提供服务的线程,也成为守护线程。如果说演员是前台线程,那么其他工作人员就是后台线程。
Java 虚拟机的垃圾回收线程就是典型的后台线程,它负责回收其他线程不再使用的内存
后台线程的特点是:后台线程和前台线程相伴相随,只有前台线程都结束生命周期,后台线程才会结束生命周期。只要有一个前台线程还没有结束运行,后台线程就不会结束生命周期。
主线程在默认情况下是前台线程,由前台线程创建的线程在默认情况下也是前台线程。调用Thread 类的 setDaemon(true)方法,就能把一个线程设置为后台线程。Thread 类的isDaemon()方法用来判读一个线程是否是后台线程。
Java 虚拟机保证:当所有前台线程运行结束后,再终止后台线程,体现的是先后顺序。那么前台线程运行结束后,后台线程是一次也不运行吗?这取决于程序的实现。
只有线程启动前(即调用 start()方法以前),才能把它设置为后台线程。如果线程启动后,再调用这个线程的 setDaemon()方法,就会导致 IllegalThreadStateException 异常。
在 JDK 的 java.util 包中提供了一个实用类 ScheduledExecutorService,它能够定时执行特定的任务。TimerTask 类表示定时器执行的一项任务。
public class TestMain {
public static void main(String[] args) throws InterruptedException {
int count = 0;
//定时任务的工具类
ScheduledExecutorService ses= Executors.newScheduledThreadPool(1);
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println("this time:"+new Date());
}
};
//利用工具类按照不同的条件触发定时任务
//设置执行一次的定时任务
ses.schedule(timerTask,5000, TimeUnit.MILLISECONDS);
//设置反复执行,固定频率的定时任务
// ses.scheduleAtFixedRate(timerTask,0,5000,TimeUnit.MILLISECONDS);
//设置反复执行,固定间隔的定时任务
// ses.scheduleWithFixedDelay(timerTask,0,5000,TimeUnit.MILLISECONDS);
//关闭ses
Thread.sleep(6000);
ses.shutdown();
}
}
创建并执行在给定延迟后启用的一次性操作。
参数:
创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在initialDelay + 2 * period 后执行,依此类推。如果任务的任何一个执行遇到异常,则后续执行都会被取消。否则,只能通过执行程序的取消或终止方法来终止该任务。如果此任务的任何一个执行要花费比其周期更长的时间,则将推迟后续执行,但不会同时执行。
参数:
创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。如果任务的任一执行遇到异常,就会取消后续执行。否则,只能通过执行程序的取消或终止方法来终止该任务。
参数:
在Java中为我们提供了Timer来实现定时任务,当然现在还有很多定时任务框架,比如说Spring、QuartZ、Linux Cron等等,而且性能也更加优越。但是我们想要深入的学习就必须先从最简单的开始。
在Timer定时任务中,最主要涉及到了两个类:Timer和TimerTask。他们俩的关系也特别容易理解,TimerTask把我们得业务逻辑写好之后,然后使用Timer定时执行就OK了。我们来看一个最基本的案例:
参考:https://www.jianshu.com/p/aeb391e4edb0
/**
* 带延迟时间的调度,只执行一次
* 调度之后可通过Future.get()阻塞直至任务执行完毕
*/
1. public ScheduledFuture> schedule(Runnable command,
long delay, TimeUnit unit);
/**
* 带延迟时间的调度,只执行一次
* 调度之后可通过Future.get()阻塞直至任务执行完毕,并且可以获取执行结果
*/
2. public ScheduledFuture schedule(Callable callable,
long delay, TimeUnit unit);
/**
* 带延迟时间的调度,循环执行,固定频率
*/
3. public ScheduledFuture> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
/**
* 带延迟时间的调度,循环执行,固定延迟
*/
4. public ScheduledFuture> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
该方法用于带延迟时间的调度,只执行一次。调度之后可通过Future.get()
阻塞直至任务执行完毕。我们来看一个例子。
class MyFrame extends TimerTask {
int count = 0;
private String name;
public MyFrame() {
}
public MyFrame(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println(name+i);
}
}
}
public class TestMain {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//定时任务的工具类
ScheduledExecutorService ses= Executors.newScheduledThreadPool(1);
System.out.println("main...");
ScheduledFuture> future = ses.schedule(new MyFrame("wkl-"), 5000, TimeUnit.MILLISECONDS);
//调用这个方法停止线程
future.get();
}
}
@Test public void test_schedule4Callable() throws Exception {
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture future = service.schedule(() -> {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task finish time: " + format(System.currentTimeMillis()));
return "success";
}, 1000, TimeUnit.MILLISECONDS);
System.out.println("schedule finish time: " + format(System.currentTimeMillis()));
System.out.println("Callable future's result is: " + future.get() +
", and time is: " + format(System.currentTimeMillis()));
}
运行看到的结果和Runnable
基本相同,唯一的区别在于future.get()
能拿到Callable
返回的真实结果。
一个线程在执行原子操作期间,应该采取措施使得其他线程不能操作共享资源,否则就会出现,共享资源被重复操作的问题。
为了保证每个线程能正常执行原子操作,Java 引入了同步机制,具体做法是在代表原子操作的程序代码前加上 synchronized 标记,这样的代码被称为同步代码块。
以上代码创建了 Object 对象 o,在同步块中,o 充当了同步锁的作用。
每个 Java 对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。当第一个线程拥有了这个同步锁,执行同步块里的代码时,其他的线程因为没有拥有这把锁,就不能执行同步块里的代码。即使该线程睡眠了,其他线程也是不能执行同步块里的代码。直到该线程执行完同步块释放了 o 的同步锁,其他线程才有机会执行同步块里的代码。
public class MyThread extends Thread {
//有 100 张票
int count = 100;
//同步锁
Object o = new Object();
public void run() {
while (true) {
synchronized (o) {
if (count > 0) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName()
+ "=" + count);
count--;
} else {
break;
}
}
}
}
}
当前对象也可以作为同步锁使用,所以也可以这样写同步块:
synchronized(this){}
当一个线程开始执行同步代码块时,并不意味着以不中断的方式运行。进入同步代码的线程也可以执行 Thread.sleep()或者执行 Thread.yield()方法,此时它并没有释放锁,只是把运行机会让给了其他的线程。
使用 synchronized 关键字修饰的方法为同步方法,同步方法和同步块一样有线程同步的功能。
public class MyThread extends Thread {
//有 100 张票
int count = 100;
//同步锁
Object o = new Object();
public void run() {
while (true) {
if (count < 1) {
break;
}
sale();
}
}
public synchronized void sale() {
if (count > 0) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "=" + count);
count--;
}
}
}
同步方法中使用当前对象 this 作为同步锁,所以不需要额外声明同步锁。
synchronized 声明不会被继承。如果一个用 synchronized 修饰的方法被子类覆盖,那么子类中这个方法不再保持同步,除非也用 synchronized 修饰。
同步是解决共享资源竞争的有效手段。当一个线程已经在操纵共享资源时,其他线程只能等待,只有当已经在操纵共享资源的线程执行同步代码后,其他线程才有机会操纵共享资源。
为了提高并发性能,应该使同步代码块中包含尽可能少的操作,使得一个线程能尽快释放锁,减少其他线程等待锁的时间。可以改为一个人打完一桶水后,就让其他人打水,大家轮流打水,直到每个人都打完 10 桶水。
一个线程安全的类满足以下条件:
这个类的对象可以同时被多个线程安全的访问。
每个线程都能正常执行原子操作,得到正确的结果。
在每个线程的原子操作都完成后,对象处于逻辑上合理的状态。