多线程知识点详解(附源码)
即使再小的帆也能远航~
一 、目录
多线程概念
Thread类
创建线程的三种方式
线程的生命周期
控制线程
并发与并行
线程同步
线程通询(等待唤醒机制)
线程池
线程相关类
线程安全问题讨论
补充
强调
二内容
多线程概念
线程与进程
- 进程
-
所有的操作系统都支持进程,当一个程序进入内存时就变成了一个进程。进程就是处于运行过程中的程序,并且具有一定的独立能力,进程是系统进行资源分配和调度的一个独立单元。
-
进程包含如下3个特征:
独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个进程不可以直接访问其他进程的地址空间。
动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。进程具有自己的生命周期和各种不同的状态,这些在程序中都是不具备的。
并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
-
对于一个CPU而言,它在某个时间点只能执行一个程序,也就是说,只能运行一个进程,CPU不断地在这些进程之间轮换执行。
- 线程
-
线程(Thread)也被称作轻量级进程,线程是进程的执行单元。类似于进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。
-
线程是独立运行的,它并不知道进程中是否存在其他的线程存在。线程的执行是抢占式的,也就是当前运行的线程在任何时候都有可能被挂起,以便另外一个线程可以运行。
-
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
-
从逻辑角度来看,多线程存在于一个应用程序中,让一个应用程序中可以有多个执行部分同时执行,但操作系统无需将多个线程看成多个独立的应用,对多线程实现调度和管理以及资源分配。因为线程的调度和管理是由进程本身负责完成。
-
线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中的线程之间的隔离程度要小。因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
-
使用多线程编程具有如下优点:
-
进程之间不能共享内存,但线程之间共享内存非常容易。
-
系统创建进程是需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
-
Java语言内置了多线程功能支持,而不是单纯的作为底层操作系统的调度方式,从而简化了Java的多线程编程。
-
Thread类
-
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
-
Thread类的常用构造方法
-
Thread() 分配新的Thread对象。 无参构造,用于创建线程的第一种方法:继承Thread类,覆盖重写run()方法
-
Thread(Runnable target) 分配新的Thread对象。 一个实现了Runnable类的类对象(或者用实现类的匿名对象)做参数(这是多态:左父右子、左接口右实现类),用于创建线程的第二种方法:一个类实现Runnable类,并覆盖重写run()方法,最后创建该类对象(或者匿名对象)来做Thread类的参数进而创建一个Thread类对象
-
-
Thread类常用方法
-
静态方法 无关类直接用 该类的类名.方法名()就可以去调用,不需要继承或者去实现
该类的子类或者实现类也可用 该类的类名.方法名()就可以去调用,但也可以直接 方法名()去调用
-
static Thread currentThread() 返回当前正在执行的线程对象
-
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
-
static void sleep(long millis, int nanos) 在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
-
static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
-
成员方法
-
String getName() 返回该线程的名称
-
int getPriority() 返回线程的优先级。
-
boolean isAlive() 测试线程是否处于活动状态。
-
void join() 等待该线程终止。 void join(long millis) 等待该线程终止的时间最长为 millis 毫秒。 void join(long millis, int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
-
void setDaemon(boolean on) 将该线程标记为守护线程或用户线程
-
void setName(String name) 改变线程名称,使之与参数 name 相同 没什么用,通常在Thread子类或者Runnable实现类里面通过有参构造把名字弄好了,一般不会去改
-
void setPriority(int newPriority) 更改线程的优先级。
-
void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法
-
-
创建线程的三种方式
-
继承Thread创建线程
-
步骤
- 创建一个类,使其继承Thread类,并重写run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run方法称为线程执行体
- 创建该类的对象(匿名对象也可),用对象(匿名对象)去调start()方法,开启线程,千万不可调用run()方法,否则就不是多线程了,而是在main()方法里面调用了其他类的一个方法
-
示例程序
一个.java文件可以有多个类,但只准一个类有public,且该类类名必须和.java的文件名一样
- public class ThreadDemo { public static void main(String[] args) { for (int i = 0; i < 5; i++) { System.out.println("线程名称:" + Thread.currentThread().getName() + "" + i); //这个线程是主线程(main线程),没有继承Thread类,所以用Thread的类名去调 if (i == 2) { new SubThread().start(); new SubThread().start(); } } } } class SubThread extends Thread { @Override public void run() { for (int i = 0; i < 2; i++) { System.out.println("线程名称:" + getName() + "" + i); //i 前面加“”的目的:把int类型的i变成String类型的i //扩展 把其他类型的变量a变为String:1.String.toString 2.a+""(或者“”+a) 3.String.valueOf } } }
-
测试结果
线程名称:main0 线程名称:main1 线程名称:main2 线程名称:main3 线程名称:main4 线程名称:Thread-10 线程名称:Thread-11 线程名称:Thread-00 线程名称:Thread-01 Process finished with exit code 0
-
-
实现Runnable接口创建线程
-
步骤
- 创建一个类,实现Runnable,覆盖重写run()方法
- 创建一个Thread类对象,创建一个该类对象,将其作为Thread类对象的参数,Runable对象仅仅作为Thread对象的target目标,Runable实现类里包含run方法,仅仅作为线程执行体,而实际的线程对象依然是Thread实例对象,只是该Thread线程负责执行其target目标的run方法而已。
- 调用线程对象的start()方法来启动该线程。
-
示例程序
public class ThreadDemo { public static void main(String[] args) { for (int i = 0; i < 5; i++) { System.out.println("线程名称:" + Thread.currentThread().getName() + "" + i); RunnableImpl rl=new RunnableImpl(); if (i == 2) { //这种是匿名对象 参数是匿名对象,线程对象也是匿名对象 new Thread(new RunnableImpl()).start(); //参数需要的是Runnable的对象,给的是Runnable实现类的对象,我理解为多态 new Thread(rl).start(); } } } } class RunnableImpl implements Runnable { @Override public void run() { for (int i = 0; i < 2; i++) { //与第一种方式相比,得到线程名称的方式发生了变化 System.out.println("线程名称:" + Thread.currentThread().getName() + "" + i); } } }
-
测试结果
线程名称:main0 线程名称:main1 线程名称:main2 线程名称:main3 线程名称:main4 线程名称:Thread-10 线程名称:Thread-11 线程名称:Thread-00 线程名称:Thread-01 Process finished with exit code 0 //同时在执行的时候变量i是连续的,这是因为通过Runnable接口的方式创建的多个线程可以共享同一个target的实例属性, //这是因为创建的多个线程都是在执行同一个目标target类。
-
-
使用Callable&Future创建线程
-
Callable,Runnable,FutureTask之间的关系:
-
FutureTask原码
public class FutureTask<V> implements RunnableFuture<V> {}
RunnableFuture原码
public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
所以FutureTask既可以看成是Runnable类型又可以看成是Future类型。
-
FutureTask原码
public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; // ensure visibility of callable }
所以Callable可以做FutureTask的参数
-
总结:FutureTask可以作为Runnable类型做Thread的参数,Callable可以做FutureTask的参数
-
-
步骤
- 创建一个类,该类实现(implement) Callable<>,重写call()
- 在主线程(main方法)里面创建该类对象,该类对象是Callable类型的,但是Thread类的参数需要的是Runnable类型的,所以需要一个"中间人"
- 创建FutureTask对象, FutureTask需要一个Callable类型的参数&FutureTask是Runnable类型,所以可以做Thread的参数,完美的做好了"中间人"的身份
- 创建一个Thread对象,将FutureTask对象作为参数传进去
- 使用Thread对象调用start()方法
- 调用FutureTask对象的get()方法来获得子线程结束后的返回值。
-
注意:Callable与FutureTask 两者的泛型V要保持一致
-
V get():V类型的成员方法
- call()方法并不是直接调用,它是作为线程执行体被调用的。但是call()方法是有返回值的,所以需要通过get()方法得到返回值
- V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束才会得到返回值
- V get(long timeout,TimeUnit unit):返回Callable任务里call()方法的返回值,该方法让程序最多阻塞timeout长的时间,unit为时间单位,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常。
-
task.get()方法为阻塞式方法,在线程没有执行结束前是没有返回值的,因此get()方法会一直等待线程的执行结束,并将数据值返回。因此在等待线程执行结束后,mian线程才会继续执行,并最终打印:线程执行结束后的返回值是多少。
-
示范程序
也可以把main方法写在 CallableImpl类里面,将两个类合为一个类
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class ThreadDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建Callable对象 Callable<Integer> call = new CallableImpl(); // 创建FutureTask对象,并将call对象封装在FutureTask内部,FutureTask的泛型为Callable // 返回值类型 FutureTask<Integer> task = new FutureTask<>(call); // 创建线程对象Thread new Thread(task).start(); // 获取线程结束后的返回值 System.out.println("线程执行结束后的返回值:" + task.get()); } } class CallableImpl implements Callable<Integer> { @Override public Integer call() throws Exception { for (int i = 0; i < 10; i++) { System.out.println("当前线程名称:" + Thread.currentThread().getName() + " " + i); // 线程休眠,每打印一次线程停顿2秒种 Thread.sleep(2000); } return 100; } }
-
测试结果
当前线程名称:Thread-0 0 当前线程名称:Thread-0 1 当前线程名称:Thread-0 2 当前线程名称:Thread-0 3 当前线程名称:Thread-0 4 当前线程名称:Thread-0 5 当前线程名称:Thread-0 6 当前线程名称:Thread-0 7 当前线程名称:Thread-0 8 当前线程名称:Thread-0 9 线程执行结束后的返回值:100 Process finished with exit code 0
-
-
创建线程的三种方式比较
Runnable,Callable实现接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常,并且Callable需要FutureTask来进行封装成Thread可识别的target目标。所以可以将其归结为一种方式
- 采用Runnable、Callable接口的方式创建多线程:
- 多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
- 采用Thread类的方式创建多线程
- 优势:编写简单,如果需要访问当前线程,直接使用this即可。
- 劣势:继承Thread类后,无法再继承其他父类。单继承,多实现!
- 采用Runnable、Callable接口的方式创建多线程:
线程的生命周期
-
在线程的生命周期中,它要经历新建、就绪、运行、阻塞和死亡5种状态.
-
新建和就绪
- 当程序使用new关键字创建一个线程之后,该线程就处于新建状态
- 当线程对象调用了start方法之后,该线程就处于就绪状态,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了
-
运行和阻塞状态
-
如果处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程就处于运行状态
-
当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,使其他线程获得执行的机会,系统会给每个可执行的线程一个小时间段来处理任务,当时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。
-
线程发生阻塞状态时的情况
- 线程调用sleep()方法主动放弃所占用的处理器资源
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
- 线程在等待某个通知(notify)
- 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
-
被阻塞的线程会在合适的时候重新进入就绪状态,合适的时候就是指线程的阻塞解除后,会重新进入就绪状态,等待线程调度器的再次调度。在发生如下特定情况下,可以解除线程的阻塞状态,让线程重新进入就绪状态
-
调用sleep()方法的线程经过了指定时间
-
线程调用的阻塞式IO方法已经返回
-
线程成功地获得了试图取得的同步监视器
-
线程正在等待某个通知时,其他线程发出了一个通知
-
处于挂起的线程被调用了resume()恢复方法
-
-
线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程就进入运行状态,当处于运行状态的线程失去处理器资源时,该线程就进入就绪状态
-
线程死亡
- run()或call()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的Exception或者直接Error错误。
- 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。
-
注意:当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动之后,它就拥有和主线程相同的地位
-
测试某个线程是否死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞3中状态时,该方法将返回true;当线程处于新建、死亡2种状态时返回false。
-
注意:不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程不可以再次作为线程执行。
-
如果线程已经死亡了,再次启动该线程,将会引发IllegalThreadStateException异常
-
控制线程
-
通过一些方法来控制线程的操作,还可以将线程设置为守护线程
join线程
-
线程A中包含了一个或多个线程,只有当线程A运行起来,执行到其他线程的start()方法,其他线程才会开启,去运行,
则线程A就被称为主线程,一般是main线程是主线程.
-
在主线程中,Thread的某个子类对象开启另一个线程(不管是继承Thread还是实现Runnable或者Callable,开启线程的一定是Thread的子类对象,Runnable或者Callable的对象只是做参数),若该线程调用join()方法,当主线程执行到join()方法时,主线程就会停止执行,等到调用join()方法的那个线程全部运行结束,主线程才会继续运行.
-
示例代码
在这个示例代码中,主线程就不是main线程,而是ManageThread
class ManageThread extends Thread { @Override public void run() { long l1 = System.currentTimeMillis(); System.out.println("准备开会,然后进入等待状态..............."); try { DepartmentOne one = new DepartmentOne(); one.start(); //DepartmentOne的对象调用join()方法,主线程ManageThread停止运行,直 //至DepartmentOne全部运行结束,耗时1000ms one.join(); DepartmentTwo two = new DepartmentTwo(); two.start(); //DepartmentTwo的对象调用join()方法,主线程ManageThread停止运行,直 //至Departmenttwo全部运行结束,耗时2000ms two.join(); DepartmentThree three = new DepartmentThree(); three.start(); //DepartmentThree的对象调用join()方法,主线程ManageThread停止运行,直 //至DepartmentThree全部运行结束,耗时3000ms three.join(); } catch (Exception e) { e.printStackTrace(); } System.out.println("人员到齐,开会!!!"); long l2 = System.currentTimeMillis(); System.out.println("运行耗费"+(l2-l1)+"ms"); } class DepartmentOne extends Thread { @Override public void run() { try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } System.out.println("部门1准备好了!"); } } class DepartmentTwo extends Thread { @Override public void run() { try { sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("部门2准备好了!"); } } class DepartmentThree extends Thread { @Override public void run() { try { sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("部门3准备好了!"); } } } public class ThreadDemo { public static void main(String[] args) { new ManageThread().start(); } }
-
运行结果
准备开会,然后进入等待状态............... 部门1准备好了! 部门2准备好了! 部门3准备好了! 人员到齐,开会!!! 运行耗费6005ms Process finished with exit code 0
-
上面代码是开启一个新线程,主线程马上停止运行,所以耗时六秒左右
如果是先开启所有线程,最后在调用join()方法,那么耗时就是最长的那个时间
-
一个进程执行ABC三个线程,A耗时t1秒,B耗时t2秒,C耗时t3秒(假设t1>t2>t3),则这个进程耗费的总时间是t1秒,也就是最长的那个时间,而不是加起来
原因:计算机的存储速度和CPU运算的速度是不匹配的,也就是说CPU的运算速度远远快于计算机存储速度。为了解决这种阻抗不匹配,计算机必须引入一种机制来解决这样的问题。计算机引入高速缓存区来作为内存与CPU之间的桥梁,首先,将需要运算的数据放置到高速缓存中,加快运算速度,运算结束后,将缓存中的数据同步到内存中去,因此CPU不需要等待内存读写。
-
代码测试
class ManageThread extends Thread { @Override public void run() { long l1 = System.currentTimeMillis(); System.out.println("准备开会,然后进入等待状态..............."); try { DepartmentOne one = new DepartmentOne(); one.start(); DepartmentTwo two = new DepartmentTwo(); two.join(); DepartmentThree three = new DepartmentThree(); three.start(); one.join(); two.start(); three.join(); } catch (Exception e) { e.printStackTrace(); } System.out.println("人员到齐,开会!!!"); long l2 = System.currentTimeMillis(); System.out.println("运行耗费" + (l2 - l1) + "ms"); } class DepartmentOne extends Thread { @Override public void run() { try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } System.out.println("部门1准备好了!"); } } class DepartmentTwo extends Thread { @Override public void run() { try { sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("部门2准备好了!"); } } class DepartmentThree extends Thread { @Override public void run() { try { sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("部门3准备好了!"); } } } public class ThreadDemo { public static void main(String[] args) { new ManageThread().start(); } }
-
运行结果
准备开会,然后进入等待状态............... 部门1准备好了! 部门2准备好了! 部门3准备好了! 人员到齐,开会!!! 运行耗费3004ms Process finished with exit code 0
-
-
join()方法常用的重载方法
join():等待被join的线程执行完成。
join(long millis):等待被join的线程的时间最长为millis毫秒。如果在millis毫秒内被join的线程没有执行结束,则不再等待
-
线程休眠
- static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态。
- 当线程调用sleep()方法进入阻塞状态后,在其睡眠时间内,该线程不会获得执行的机会,即使线程中没有其他可以执行的线程,处于sleep()中的线程也不会执行
- sleep()是静态方法,由类(Thread)去调,也就是说:sleep()方法写在哪个线程里面,就对哪个线程起作用
线程让步
-
static void yield() ;
-
yield()方法让当前线程暂停,但是不阻塞线程而是让当前线程进入就绪状态。也就是说,对某个线程使用yield()方法之后,该线程会退出运行,和其他处于就绪状态的线程争夺CPU执行权,如果抢到了依旧可以再进去.² 因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。
-
yield()是静态方法,由类(Thread)去调,也就是说:sleep()方法写在哪个线程里面,就对哪个线程起作用
改变线程的优先级
- 每个线程执行都具有一定的优先级,优先级较高的线程会获得更多的执行机会,而优先级较低的线程则获得较少的执行机会。
- 每个线程默认的优先级都与创建它的父线程优先级相同,在默认的情况下main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。
- Thread类提供了setPriority(int newPriority),getPriority()方法来设置和获取指定线程的优先级,其中setPriority方法的参数可以是一个整数,范围在1~10之间,可以使用Thread类的三个静态常量:MAX_PRIORITY:其值为10,MIN_PRIORITY:其值为1,NORM_PRIORITY:其值为5。
后台线程
-
有一种线程在后台运行,它的任务是为其他线程提供服务,这种线程被称为“后台线程”或者“守护线程”。JVM的垃圾回收线程就是典型的后台线程。
-
后台线程的特征:如果所有的前台线程死亡,后台线程就会自动死亡。
-
调用Thread对象的setDaemon(true)方法可以将指定线程设置为后台线程。
这是一个成员方法,Thread的哪个子类对象来调它,那这个线程就是后台线程
并发与并行(很重要!!!)
-
并发:两个或多个事件在同一时间段内发生(交替执行)
- 例:一个人吃两个馒头
-
并行:两个或多个事件在同一时刻发生(同时发生)
- 例:两个人吃两个馒头
-
在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。
也就是说,CPU是在多个线程之间来回快速跳转的.进程和线程是并发运行的,OS的线程调度机制将时间划分为很多时间片段(时间片),尽可能均匀分配给正在运行的程序,获取CPU时间片的线程或进程得以被执行,其他则等待
总结: ==============================================>进程
|| || || ||
|| || || ||
|| || || ||
|| || || ||
主线程 子线程A 子线程B 子线程C
因为CPU执行速度非常快,所以多个线程之间启动(调用start()方法)的时间差可以忽略不计,所以
#### 可以认为多个线程是同时启动的,然后CPU在这几个线程之间来回快速跳转,跳转到谁哪,谁就执行一点儿
多线程属于并发编程;多线程运行所花费的总时间是最长的那个时间,而不是时间和(没有join()方法的情况下)
线程同步
多线程访问共享数据时容易导致线程安全问题
重要:
-
示例代码
public class ThreadDemo { public static void main(String[] args) { Account account=new Account(1000, "gyz"); new SubThread("5366", 800, account).start(); new SubThread("152", 800,account).start(); } } class Account { private double money; private String name; public Account(double money ,String name) { this.money = money; this.name=name; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } } class SubThread extends Thread{ private double number; //类做成员变量 即关联/依赖关系 private Account account; public SubThread(String name, double number,Account account) { super(name); this.number = number; this.account=account; } @Override public void run() { if (account.getMoney()>=number){ try { //这个线程休眠很重要,大大的增加了出现线程安全的概率!!! sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //对共享数据作出修改 account.setMoney((account.getMoney()-number)); System.out.println("您在"+getName() +"号柜台取出"+number+"元,账户余额为:"+account.getMoney()); }else{ System.out.println("余额不足"); } } }
-
测试结果
您在5366号柜台取出800.0元,账户余额为:200.0 您在152号柜台取出800.0元,账户余额为:-600.0 Process finished with exit code 0 =================================================== 您在152号柜台取出800.0元,账户余额为:-600.0 您在5366号柜台取出800.0元,账户余额为:-600.0 Process finished with exit code 0 =================================================== 您在5366号柜台取出800.0元,账户余额为:200.0 您在152号柜台取出800.0元,账户余额为:200.0 Process finished with exit code 0
-
代码分析
-
先说一个问题:如果没有sleep(1000)这个方法,出现线程安全问题的概率并不大,但是有了sleep(1000)这个方法,线程出现安全问题概率贼大.
很很重要!!!
1.出现问题的原因:一个线程抢到CPU执行权之后,就会去运行run()方法, 设当运行到if (account.getMoney()>=number)时间点为t1,运行到 account.setMoney((account.getMoney()-number)); System.out.println("您在"+getName() +"号柜台取出"+number+"元,账户余额为:"+account.getMoney())时间点为 t2,==>很自然的就会发现t1,t2之间存在时间差;一个线程经过了时间点t1,但是还未到时间点t2,在这个过程中,其他线程经过了时间 点t1,所以当该线程过了时间点t2,即使现在的余额money已经小于其他线程要取的钱数number,但是其他线程已经过了时间点t1,所以 也仍然会去 account.setMoney((account.getMoney()-number)); System.out.println("您在"+getName() +"号柜台取出"+number+"元,账户余额为:"+account.getMoney()) 导致出现余额为负数这种情况; 相同的余额就是因为同时经过了时间点t1和t2;sleep() 2.方法会提高安全问题出现的概率的原因:sleep()方法使得时间点t1和时间点t2之间的时间差增大了,所以导致最早获得CPU执行权的线 程在还未经过时间点t2之前经过时间点t1的线程多了, 没有线程经过时间点t2,就没有线程可以改变money,就导致后面满足 if (account.getMoney()>=number)这个条件的线程增多,就进去了,但在后面线程过了t1去t2的路上这个时间段,前面的线程就到 了t2,改变了money,导致money小于后面的number,出现负数
-
同步代码块( 注:同步是一种高开销的操作,因此应该尽量减少同步的内容。)
-
为了解决上述问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:
synchronized(obj){ // 此处的代码为同步代码 }
在上面语法格式中的obj就是同步监视器,又叫锁对象,对象锁,对象监视器,其含义就是:线程在开始执行同步代码块之前,必须先获得对同步监视器的锁定.
-
Java程序允许使用任何对象作为同步监视器,但是同步监视器的目的是为了:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被多个线程并发访问的共享资源来充当同步监视器
-
,同步块的锁是可以选择的,但是不是可以任意选择的!!!!这里必须要注意一个物理对象和一个引用对象的实例变量之间的区别!使用一个引用对象的实例变量作为锁并不是一个好的选择,因为同步块在执行过程中可能会改变它的值,其中就包括将其设置为null,而对一个null对象加锁会产生异常,并且对不同的对象加锁也违背了同步的初衷!必须注意:同步是基于实际对象而不是对象引用的!多个变量可以引用同一个对象,变量也可以改变其值从而指向其他的对象,因此,当选择一个对象锁时,我们要根据实际对象而不是其引用来考虑!作为一个原则,不要选择一个可能会在锁的作用域中改变值的实例变量作为锁对象!
-
同步代码块理解:
-
当一个线程执行到同步代码块,遇到synchronized的时候,这个线程就会去看看括号里有没有对象锁,如果有,就拿走对象锁(加锁操作);去同步代码块里面继续运行,其他线程来了以后发现没有对象锁,就没法进入同步代码块里面执行,就只能等着,等到进入同步代码块的线程运行完以后归还对象锁(释放对同步监视器的锁定),那么其他线程才可以拿着对象锁进去运行
通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进行修改共享资源的代码区(也被称为临界区),所以同一时刻最多有一个线程处于临界区内,从而保证了线程的安全性
-
-
同步代码块的位置:
同步代码块的作用是保证任意时刻只有一个线程可以对共享资源的代码区(临界区)进行修改,也就是说同步代码块里的同步代码就是共享资源,而共享资源是要被多个多个线程访问的(只是每个时刻只能被一个线程访问,不能同时而已),所以共享资源要在Thread子类的run()方法里,所以同步代码块也是在Thread子类的run()方法里
-
示例代码
public class ThreadDemo { public static void main(String[] args) { Account account = new Account(1000, "gyz"); new SubThread("5366", 800, account).start(); new SubThread("152", 800, account).start(); } } class Account { private double money; private String name; public Account(double money, String name) { this.money = money; this.name = name; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } } class SubThread extends Thread { private double number; private Account account; public SubThread(String name, double number, Account account) { super(name); this.number = number; this.account = account; } @Override public void run() { synchronized (account) { if (account.getMoney() >= number) { try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //对共享数据作出修改 account.setMoney((account.getMoney() - number)); System.out.println("您在" + getName() + "号柜台取出" + number + "元,账户余额为:" + account.getMoney()); } else { System.out.println("余额不足"); } } } }
-
测试结果
您在5366号柜台取出800.0元,账户余额为:200.0 余额不足 Process finished with exit code 0
-
雷区
不能把同步代码块写到其他类的一个方法中,再调进Thread子类的run()方法里!!!这样不行这样写代码程序会报很多错,可能也有办法运行,但太麻烦了
同步方法( 注:同步是一种高开销的操作,因此应该尽量减少同步的内容。)
-
同步方法就是使用synchronized关键字来修饰某个方法,则此方法被称为同步方法。
-
对于synchornized修饰的对象方法而言,无须显示的指定它同步监视器,
同步方法的同步监视器就是当前this指向的对象,也就是调用该方法的对象。**同步方法一般是成员方法,没有static修饰,谁来调用呢?**写在哪个类就由哪个类的对象来调
同步方法的位置:方法内不能写方法,所以写在其他类里面,然后在Thread的子类里面创建一个该类的对象(关联/依赖关系),由该对象去run()方法里面调用同步方法
-
示例代码
public class ThreadDemo { public static void main(String[] args) { Account account = new Account(1000, "gyz"); new SubThread("5366", 800, account).start(); new SubThread("152", 800, account).start(); } } class Account { private double money; private String name; public synchronized void Withdrawal(double number) { if (money >= number) { money -= number; System.out.println("您在" + Thread.currentThread().getName() + "号柜台取出" + number + "元,账户余额为:" + money); } else { System.out.println("余额不足"); } } public Account(double money, String name) { this.money = money; this.name = name; } } class SubThread extends Thread { private double number; private Account account; public SubThread(String name, double number, Account account) { super(name); this.number = number; this.account = account; } @Override public void run() { account.Withdrawal(number); } }
-
测试结果
您在5366号柜台取出800.0元,账户余额为:200.0 余额不足 Process finished with exit code 0
-
通过使用同步方法可以非常方便地实现线程安全的类(同步方法所在的类是线程安全类)
线程安全的类具有如下特征:
该类的对象可以被多个线程安全地访问。
每个线程调用该对象的任意方法之后都将得到正确的结果。
每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
-
!!!雷区
同步方法不能写在Thread的子类里面,然后再去run()方法里面调用!!这样不行!!!原因:如果要想使线程安全,就应该使共享数据的代码所在的类是线程安全的类,而同步方法所在的类是线程安全类,所以应该把同步方法写在共享资源的代码区的那个类中
- 把同步方法写在Thread的子类里面的错误代码演示
public class ThreadDemo { public static void main(String[] args) { Account account = new Account(1000, "gyz"); new SubThread("5366", 800, account).start(); new SubThread("152", 800, account).start(); } } class Account { private double money; private String name; public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } public Account(double money, String name) { this.money = money; this.name = name; } } class SubThread extends Thread { private double number; private Account account; public SubThread(String name, double number, Account account) { super(name); this.number = number; this.account = account; } public synchronized void Withdrawal(double number) throws InterruptedException { if (account.getMoney() >= number) { sleep(1000); account.setMoney((account.getMoney()-number)); System.out.println("您在" + Thread.currentThread().getName() + "号柜台取出" + number + "元,账户余额为:" +account.getMoney()); } else { System.out.println("余额不足"); } } @Override public void run() { try { this.Withdrawal(number); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
运行结果
您在152号柜台取出800.0元,账户余额为:-600.0 您在5366号柜台取出800.0元,账户余额为:-600.0 Process finished with exit code 0 ================================================= 您在152号柜台取出800.0元,账户余额为:200.0 您在5366号柜台取出800.0元,账户余额为:200.0 Process finished with exit code 0
-
synchronized与Lock的区别:
同步方法或者同步代码块使用隐式的同步监视器对象,并且强制要求加锁操作和释放锁操作要出现在一个代码块结构中。而且当获取了多个锁时,他们必须以相反的顺序释放,并且必须在与所有锁被获取时的相同的代码块范围内释放所有锁。
释放同步监视器的锁定
- 线程会在以下几种情况来释放对同步监视器的锁定。
- 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
- 当前线程的同步代码块、同步方法中遇到break、return终止该代码块,当前线程将会释放同步监视器。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或者Exception,导致了该代码块、该方法异常结束时,当期线程将会释放同步监视器。
- 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
- 如果出现以下情况下,线程不会释放同步监视器。
- 线程执行同步代码块或同步方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。
同步锁
-
从JDK 1.5开始,Java提供了一种功能更强大的线程同步机制,同步锁Lock
-
同步监视器(对象锁,锁对象)使用Lock对象来充当
-
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁(一个线程拿到lock锁进到临界区),线程开始访问共享资源之前应先获得Lock对象。
-
某些锁可能允许对共享资源的并发访问,例如ReadWriteLock读写锁就允许并发访问
同步锁基本语法(绝了):
最好不要把获取锁的过程写在try语句块中,因为如果在获取锁时发生了异常,异常抛出的同时也会导致锁无法被释放。
private ReentrantLock lock = new ReentrantLock();//获取锁 必须写在方法外面!!! 这样就可以一个锁对象建立多个同步锁 public void method() {//同步锁整个过程是写在一个方法当中的!!!!!!! lock.lock();// 加锁 最先过来的线程在这就拿到了同步监视器(lock锁对象),其他线程再过来的时候,已经没有lock锁了,这能等着 try{ //临界区 }finally{ lock.unlock(); // 释放锁 最先进来的线程释放对象锁,在上面加锁的地方等着的线程可以拿着锁进来了 } }
易错点:private ReentrantLock lock = new ReentrantLock(); 这个必须要写在同步锁所在的方法的外面!!!
-
同步锁的位置:与同步方法位置相同!!
强调一下:同步锁是写在一个方法中,调用进run()方法的!!!
-
示例代码:
public class ThreadDemo { public static void main(String[] args) { Account account = new Account(1000, "gyw"); new SubThread("5132", account, 800).start(); new SubThread("5872", account, 800).start(); } } class Account { private double money; private String name; public Account(double money, String name) { this.money = money; this.name = name; } ReentrantLock lock = new ReentrantLock(); public void Withdrawal(double num) { lock.lock(); try { if (money >= num) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } money -= num; System.out.println("您在" + Thread.currentThread().getName() + "号柜台" + "取款,取出" + num + "元,余额为" + money + "元"); } else { System.out.println("余额不足!"); } } finally { lock.unlock(); } } } class SubThread extends Thread { private Account account; private double number; public SubThread(String name, Account account, double number) { super(name); this.account = account; this.number = number; } @Override public void run() { account.Withdrawal(number); } }
-
测试结果:
您在5132号柜台取款,取出800.0元,余额为200.0元 余额不足! Process finished with exit code 0
死锁
线程通讯(等待唤醒机制)
传统的线程通讯
-
使用的是同步监视器(同步代码块或者同步方法)
-
借助Object类提供的wait()、notify()和notifyAll()3个方法,这3个方法必须由同步监视器对象来调用
- wait():导致当前线程进入等待,直到其他线程调用该同步监视器的notify()方法或者notifyAll()方法来唤醒该线程。调用wait方法的当前线程会释放对该同步监视器的锁定
- notify():唤醒此同步监视器上等待的单个线程。如果所有的线程都在此同步监视器上等待着,则会唤醒其中一个线程。选择是任意的。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
- notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
-
代码示例:
public class ThreadDemo { public static void main(String[] args) { BankAccount account = new BankAccount(); new DispoteThread(account).start(); new DrawThread(account).start(); } } class BankAccount { // flag为false表示账户中没有钱 private boolean flag = false; // 取款操作的方法 public synchronized void drawMoney() throws Exception { if (this.flag) { System.out.println("进行取款操作,取走金额为800,当前账户余额为0!"); this.flag = false; Thread.sleep(1000); this.notifyAll(); } else { this.wait(); } } // 取款操作的方法 public synchronized void dispoteMoney() throws Exception { if (this.flag) { this.wait(); } else { System.out.println("进行存款操作,存款金额为800,当前账户余额为800!"); this.flag = true; Thread.sleep(1000); this.notifyAll(); } } } class DrawThread extends Thread{ private BankAccount account = null; public DrawThread(BankAccount account ) { this.account = account; } @Override public void run() { try { while(true) { this.account.drawMoney(); } } catch (Exception e) { e.printStackTrace(); } } } class DispoteThread extends Thread{ private BankAccount account = null; public DispoteThread(BankAccount account ) { this.account = account; } @Override public void run() { try { while(true) { this.account.dispoteMoney(); } } catch (Exception e) { e.printStackTrace(); } } }
-
测试结果
进行存款操作,存款金额为800,当前账户余额为800! 进行取款操作,取走金额为800,当前账户余额为0! 进行存款操作,存款金额为800,当前账户余额为800! 进行取款操作,取走金额为800,当前账户余额为0! 进行存款操作,存款金额为800,当前账户余额为800! 进行取款操作,取走金额为800,当前账户余额为0! ...........以下循环执行...........
使用condition控制的线程通讯
-
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。
-
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
-
Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。
-
Condition实例被绑定在一个Lock对象上。要获得特定Lock对象的Condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了如下三个方法。
- await():类似于隐式同步监视器上的wait()方法
- signal():唤醒在此Lock对象上等待的单个线程。如果多有的线程都在该Lock对象上等待,则会唤醒其中一个线程。选择是任意的,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
- signalAll():唤醒在此Lock对象上等待的所有线程,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
-
示例代码:
public class ThreadDemo { public static void main(String[] args) { BankAccount account = new BankAccount(); new DispoteThread(account).start(); new DrawThread(account).start(); } } class BankAccount { // flag为false表示账户中没有钱 private boolean flag = false; // 创建Lock对象 private ReentrantLock lock = new ReentrantLock(); // 创建Condition对象 private Condition condition = lock.newCondition(); // 取款操作的方法 public void drawMoney() throws Exception { lock.lock(); if (this.flag) { System.out.println("进行取款操作,取走金额为800,当前账户余额为0!"); this.flag = false; Thread.sleep(1000); condition.signalAll(); } else { condition.await(); } lock.unlock(); } // 取款操作的方法 public void dispoteMoney() throws Exception { lock.lock(); if (this.flag) { condition.await(); } else { System.out.println("进行存款操作,存款金额为800,当前账户余额为800!"); this.flag = true; Thread.sleep(1000); condition.signalAll(); } lock.unlock(); } } class DrawThread extends Thread{ private BankAccount account = null; public DrawThread(BankAccount account ) { this.account = account; } @Override public void run() { try { while(true) { this.account.drawMoney(); } } catch (Exception e) { e.printStackTrace(); } } } class DispoteThread extends Thread{ private BankAccount account = null; public DispoteThread(BankAccount account ) { this.account = account; } @Override public void run() { try { while(true) { this.account.dispoteMoney(); } } catch (Exception e) { e.printStackTrace(); } } }
-
测试结果
进行存款操作,存款金额为800,当前账户余额为800! 进行取款操作,取走金额为800,当前账户余额为0! 进行存款操作,存款金额为800,当前账户余额为800! 进行取款操作,取走金额为800,当前账户余额为0! ......................................
使用阻塞队列控制的线程通讯
-
JDK 1.5提供了一个BlockingQueue接口,是作为线程同步的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞(不再抢夺CPU的执行权);当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
-
BlockingQueue提供如下两个支持阻塞的方法:
- put(Element e):尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则放弃更改队列,阻塞线程。
- take():尝试从BlockingQueue的头部取出该元素,如果该队列的元素已空,则阻塞该线程。
-
BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方法。这些方法归纳起来可分为如下三类:
- 在队列尾部插入元素。包括add、offer、put方法,当队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
- 在队列头部删除并返回该删除的元素。包括remove、poll、take方法,当队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
- 在队列头部取出但不删除元素,包括element、peek方法,当队列已空时,这两个方法分别抛出异常、返回false。
-
BlockingQueue包含如下5个实现类:
- ArrayBlockingQueue:基于数组实现的BlockingQueue队列。
- LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
- PriorityBlockingQueue:它并不是标准的阻塞队列。与前面介绍的PriorityQueue类似,该队列调用了remove、poll、take方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。
- SynchronousQueue:同步队里,该队列的存、取操作必须交替进行。
- DelayQueue:它是一个特殊BlockingQueue,底层基于PriorityBlockingQueue实现。不过DelayQueue要求集合元素都实现Delay接口,DelayQueue根据集合元素的getDelay返回值进行排序。
-
示例代码:
public class ThreadDemo { public static void main(String[] args) { ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1); new Producer(queue).start(); new Customer(queue).start(); } } //生成者代码 class Producer extends Thread{ private ArrayBlockingQueue<String> queue = null; public Producer(ArrayBlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { for(int i=0;i<3;i++) { try { String value = "产品"+i; this.queue.put(value); System.out.println("生产者放入产品:"+value); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } } //消费者代码 class Customer extends Thread{ private ArrayBlockingQueue<String> queue = null; public Customer(ArrayBlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { for(int i=0;i<3;i++) { try { String value = this.queue.take(); System.out.println("消费者取走产品:"+value); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } }
-
测试结果
生产者放入产品:产品0 消费者取走产品:产品0 生产者放入产品:产品1 消费者取走产品:产品1 生产者放入产品:产品2 消费者取走产品:产品2 Process finished with exit code 0
线程池
-
系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情况下,使用线程池可以很好的提高性能,尤其是当程序中需要创建大量存在周期很短暂的线程时,更应该考虑线程池。
-
与数据库连接池比较相似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runable对象或者Callable对象传递给线程池,线程池就会启动一个线程来执行它们的run方法或者call方法。当run方法或者call方法执行结束后,线程并不会死亡,而是再次返回线程池成为一个空闲线程,等待执行下一个Runable的run方法或者Callable对象的call方法。
-
使用线程池可以有效的控制系统中并发线程的数量。当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池最大线程数参数可以控制系统中并发线程的数量不会超过此数。
-
Java5增加了一个Executors工厂类来生成线程池,该工厂类提供了如下几个静态方法来创建线程池
- newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存到线程池中。
- newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
- newSingleThreadExecutor():创建一个只有单线程的线程池,相当于调用了newFixedThreadPool方法时,只传入了1.
- 这3个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runable对象或者Callable对象所代表的线程
- newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务,corePoolSize是指线程池中的线程数,即使线程是空闲的也会被保存线程池中。
- newSingleThreadScheduleExecutor():创建只有一个线程的线程池,它可以在指定的延迟后执行线程任务。
- 这2个方法返回一个ScheduleExecutorService线程池,它是ExecutorService子类,它可以在指定延迟后执行线程任务。
-
ExecutorService代表尽快执行线程的线程池,程序只要将一个Runable对象或者Callable对象提交给线程池,该线程池就会尽快的执行该任务。ExecutorService提供如下3个方法:
- Future submit(Runable target):将一个Runable对象提交给线程池,线程池将会在有空闲线程的时候执行该Runable对象的代表的任务。Future代表执行Runable任务的返回值,但是Runable的run方法是没有返回值的,所以Future对象在run方法执行完成后返回null,但是可以调用Future的isDone、isCancelled的方法来获得Runable对象的执行状态
- Future submit(Runable task,T result):将一个Runable对象提交给指定的线程池,线程池将在有空闲线程时执行Runable对象代表的任务。其中result为显示的指定线程执行结束后的返回值,所以Future对象将在run方法执行结束后返回result
- Future submit(Callable task):将一个Callable对象提交给指定的线程池,线程池将在有空闲线程时执行Callable对象代表的任务。其中Future代表Callable对象里的call方法的返回值
-
ScheduleExecutorService代表可在指定的延迟后或者周期性的执行线程任务的线程池,它提供了如下4个方法:
- ScheduledFuture schedule(Callable call,long delay,TimeUnit unit):指定callable任务将在delay延迟后执行,unit为时间单位。
- ScheduledFuture schedule(Runable command,long delay,TimeUnit unit):指定command任务将在delay延迟后执行,unit为时间单位。
- ScheduledFuture scheduleAtFixedRate(Runable command,long delay,long period,TimeUnit unit):指定command任务将在delay延迟后执行,而且以设定的频率重复执行。也就是说在delay后开始执行,并且在delay+period、delay+2*period…处进行重复执行,依次类推。
- ScheduledFuture scheduleWithFixedDelay(Runable command,long delay,long period,TimeUnit unit):创建并执行一个在给定初始延迟后首次启用任务的定期操作,随后在每一次终止和下次开始之间都存在给定的延迟。如果任务在任意一次执行时遇到异常,就会取消后续的操作;否则只能通过程序来进行显示的取消或终止该任务。
-
当用完一个线程池后,应该调用该线程池的shutdown方法,该方法将启动线程池的关闭序列,调用shutdown方法后的线程池不再接受新任务,但会将以前所有已提交任务执行完成。当线程池中的所有任务都执行完成后,池中的所有线程都会死亡。另外也可以调用线程池的shutdownNow的方法来关闭线程池,该方法会试图停止所有正在执行的活动线程,暂停处理正在等待的任务,并返回正在等待执行的任务列表。
-
使用线程池来执行线程任务的步骤如下:
- 调用Executors类的静态工厂方法来创建一个ExecutorService对象,该对象代表一个线程池。
- 创建Runable的实例对象或Callable的实例对象,作为线程执行的任务。
- 调用ExecutorService的submit方法来提交Runable或者Callable对象的实例。
- 当不想提交任何任务时,调用ExecutorService的shutdown方法来关闭线程池。
-
示例代码
public class ThreadDemo { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(5); pool.submit(new ThreadPoolDemo()); pool.submit(new ThreadPoolDemo()); // 关闭线程池,在线程池中没有执行完的任务继续执行,排队等待的任务取消执行 pool.shutdown(); } } class ThreadPoolDemo implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("线程名称:" + Thread.currentThread().getName() + " " + i); try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } }
-
测设结果
public class ThreadDemo { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(5); pool.submit(new ThreadPoolDemo()); pool.submit(new ThreadPoolDemo()); // 关闭线程池,在线程池中没有执行完的任务继续执行,排队等待的任务取消执行 pool.shutdown(); } } class ThreadPoolDemo implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("线程名称:" + Thread.currentThread().getName() + " " + i); try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } }
线程相关类
ThreadLocal
-
为每一个使用该变量的线程提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度去看,就好像每一个线程都拥有该变量一样。使用这个工具类可以很简洁的隔离多线程程序的竞争资源。
-
从JDK 1.5开始,Java引入了泛型,Java就为该ThreadLocal类增加了泛型支持,即:ThreadLocal
-
方法
T get() 返回此线程局部变量的当前线程副本中的值。 void remove() 移除此线程局部变量当前线程的值。 void set(T value) 将此线程局部变量的当前线程副本中的值设置为指定值。
-
ThreadLocal与普通线程同步的区别
- 在普通的同步机制中,是通过对对象加锁来先实现多个线程对同一变量的安全访问的。该变量是多个线程共享的,所以要使用这种同步机制,需要很细致的分析在什么时候需要对变量进行读写,什么时候需要对变量加锁,什么时候释放该对象的锁等。在这种情况下,系统并没有将这份资源复制多份,只是采用了安全机制来控制对这份资源的访问而已。用同一份共享资源
- ThreadLocal将需要并发访问的资源复制为多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本,自己用自己的副本,谁也别管谁
-
示例代码
/** * 在同一个线程内,资源共享类 */ public class ShareResource { public static ThreadLocal<Integer> local = new ThreadLocal<>(); } 线程类代码 public class LocalThread extends Thread{ private Integer value = null; public LocalThread(Integer value) { this.value = value; } @Override public void run() { ShareResource.local.set(value); for(int i=0;i<5;i++) { System.out.println("线程名称:"+this.getName()+" "+ShareResource.local.get()); try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } }
-
测试结果
线程名称:Thread-1 7 线程名称:Thread-0 5 线程名称:Thread-1 7 线程名称:Thread-0 5 线程名称:Thread-1 7 ..........反复执行...........
-
ThreadLocal并不能代替同步机制,两者面向的问题领域不同,同步机制是为了解决多个线程对相同资源的并发访问,是多个线程之间进行通讯的有效方式。而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免了多个线程之间共享资源的竞争,也就不需要对多个线程进行同步了。
注意:如果多个线程之间需要共享资源,以达到线程之间通信功能,就应该使用同步机制;如果仅仅是需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal。
线程安全集合类
- 从JDK 1.5开始,在java.util.concurrent包下提供了大量支持高效并发访问的接口和实现类,这些线程安全的集合类可以大致分为如下两类:
- 以Concurrent开头的集合类,如:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLikedQueue、ConcurrentLinkedDuque.
- 代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是安全的,但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此并发写入时有较好的性能。
- 当多个线程共享访问一个公共集合时,ConcurrentLinkedQueue是一个不错的选择,ConcurrentLinkedQueue不允许使用null元素。ConcurrentLinkedQueue实现了多线程的高效访问,多个线程访问ConcurrentLinkedQueue集合时无需等待。
- ConcurrentHashMap默认支持16个线程的并发写入,当超过16个线程并发向该Map写入数据时,可能有一些线程需要等待,在这种情况下,可以通过设置concurrencyLevel构造参数,来支持更多的并发写入线程
- ConcurrentLinkedQueue和ConcurrentHashMap支持线程多并发访问,所以当使用迭代器来遍历集合元素时,该迭代器可能不能反映出创建迭代器之后所做的修改,但程序不会抛出异常。
- 以CopyOnWrite开头的集合类,如:CopyOnWriteArrayList、CopyOnWriteArraySet等
- 以Concurrent开头的集合类,如:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLikedQueue、ConcurrentLinkedDuque.
定时器
-
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在Java中,可以通过Timer和TimerTask类来实现任务调度的功能。
-
TimerTask类:
-
TimerTask是调度任务类,该类为抽象类。当创建一个任务时,需要将TimerTask的run方法进行重写,如果需要取消任务时,需要调用任务类的cancel方法。
-
常用方法
public abstract void run(): 调度任务的任务体,当执行该任务时,就是调用run方法来执行。 public boolean cancel(): 取消任务的执行。当只调用一次任务且任务还未执行时,则任务被取消不再执行。如果任务被多次调用,且任务正在执行中,则 执行完当前任务,后续不再执行。
-
-
Timer类:
需要注意的是Timer的实现类有两个:分别是java.util包下的Timer类,以及java.swing包下的Timer类。在这里所使用的是java.util.Timer类。
-
常用构造方法
Timer() 创建一个新计时器。
-
常用方法
public void schedule(TimerTask task, long delay): 开启任务调度,该任务在delay毫秒后执行,只调度一次。 public void schedule(TimerTask task, long delay, long period): 开启任务调度,该任务在delay毫秒后执行,每隔period毫秒调度一次,直到被取消。
注意:如果执行任务所消耗的时间超过了间隔时间,当任务执行结束后会立刻执行下次任务
-
-
示例代码
public class TimerDemo { public static void main(String[] args) { TimerTask task = new TimerTask() { @Override public void run() { System.out.println("开始执行任务"); try { Thread.sleep(3000); } catch (Exception e) { e.printStackTrace(); } System.out.println("任务结束"); } }; Timer timer = new Timer(); timer.schedule(task, 1000, 1000); } }
-
线程安全问题讨论
volatile
- volatile关键字是Java虚拟机中最轻量级的同步机制,它确保了对一个变量的更新在其他线程是可见的。当一个变量声明为volatile,编译器运行时会监视这个变量,读一个volatile变量总会返回某个线程写入的最新值。
- 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有**原子性.**比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
- 需要注意的是,虽然对于volatile所修饰的变量能够保证线程间的可见性,但并不是说所有的情况就不需要加锁了
补充
sleep()和yield()方法是静态方法,由Thread去调,写在哪个线程里面哪个线程就执行其功能,join()和start()是成员方法,由对象去调
sleep()方法不会导致当前线程释放同步监视器,而wait()可使当前线程释放对同步监视器的锁定,wait 方法必须在 synchronized 保护的同步代码中使用
同步代码块的位置:Thread子类的run()方法中
同步方法位置:共享资源的代码区(临界区)所在的那个类
lock锁位置:共享资源的代码区(临界区)所在的那个类
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放(重要)
线程同步优先使用顺序:Lock >同步代码块(已经进入了方法体,分配了相应资源)>同步方法
强调
同步方法或者同步代码块使用隐式的同步监视器对象(不用手动的加锁解锁),lock锁是手动的